@glandais/vcyclist-engine 2.0.0 → 3.1.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.
- package/README.md +110 -25
- package/fit-kotlin-sdk.js +13575 -0
- package/fit-kotlin-sdk.js.map +1 -0
- package/kotlin-kotlin-stdlib.js +2975 -2174
- package/kotlin-kotlin-stdlib.js.map +1 -1
- package/kotlin_org_jetbrains_kotlin_kotlin_dom_api_compat.js.map +1 -1
- package/kotlinx-atomicfu.js +22 -22
- package/kotlinx-atomicfu.js.map +1 -1
- package/kotlinx-browser.js.map +1 -1
- package/kotlinx-coroutines-core.js +1224 -1224
- package/kotlinx-coroutines-core.js.map +1 -1
- package/kotlinx-serialization-kotlinx-serialization-core.js +1 -1
- package/kotlinx-serialization-kotlinx-serialization-core.js.map +1 -1
- package/package.json +7 -8
- package/vcyclist-elevation.js +1035 -972
- package/vcyclist-elevation.js.map +1 -1
- package/vcyclist-engine.d.ts +8 -3
- package/vcyclist-engine.js +913 -761
- package/vcyclist-engine.js.map +1 -1
- package/vcyclist-fit.js +563 -376
- package/vcyclist-fit.js.map +1 -1
- package/vcyclist-gpx.js +1360 -1119
- package/vcyclist-gpx.js.map +1 -1
- package/xmlutil-core.js +2983 -2950
- package/xmlutil-core.js.map +1 -1
- package/xmlutil-serialization.js.map +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@glandais/vcyclist-engine)
|
|
4
4
|
[](https://www.npmjs.com/package/@glandais/vcyclist-elevation)
|
|
5
5
|
[](https://central.sonatype.com/artifact/io.github.glandais/vcyclist-engine)
|
|
6
|
+
[](https://central.sonatype.com/artifact/io.github.glandais/vcyclist-elevation)
|
|
6
7
|
[](https://central.sonatype.com/artifact/io.github.glandais/vcyclist-gpx)
|
|
7
8
|
[](https://central.sonatype.com/artifact/io.github.glandais/vcyclist-fit)
|
|
8
9
|
[](https://central.sonatype.com/artifact/io.github.glandais/vcyclist-map)
|
|
@@ -40,14 +41,33 @@ TypeScript library for elevation data.
|
|
|
40
41
|
|
|
41
42
|
| Module | Purpose | Targets |
|
|
42
43
|
|---|---|---|
|
|
43
|
-
| **`:elevation`** | Terrarium tile fetch + DEM lookup + Haversine + Douglas-Peucker 3D + triangular smoother. See [`elevation/README.md`](elevation/README.md). | JVM, JS Node, JS browser,
|
|
44
|
-
| **`:gpx`** | Path model (36 fields × `DoubleArray`), resamplers, Douglas-Peucker simplifier, elevation steps, GPX I/O. Published to Maven Central; **not** published to npm — its JS output ships inside `@glandais/vcyclist-engine`. | JVM, JS Node, JS browser,
|
|
45
|
-
| **`:engine`** | Physics (4 resistive `PowerProvider`s + cyclist input + `MaxSpeedComputer` + `VirtualizeService`), `Enhancer` pipeline, JVM CLI, JS
|
|
46
|
-
| **`:fit`** | Garmin FIT encoding. `FitCourse` model
|
|
44
|
+
| **`:elevation`** | Terrarium tile fetch + DEM lookup + Haversine + Douglas-Peucker 3D + triangular smoother. See [`elevation/README.md`](elevation/README.md). | JVM, JS Node, JS browser, WASI |
|
|
45
|
+
| **`:gpx`** | Path model (36 fields × `DoubleArray`), resamplers, Douglas-Peucker simplifier, elevation steps, GPX I/O. Published to Maven Central; **not** published to npm — its JS output ships inside `@glandais/vcyclist-engine`. | JVM, JS Node, JS browser, WASI |
|
|
46
|
+
| **`:engine`** | Physics (4 resistive `PowerProvider`s + cyclist input + `MaxSpeedComputer` + `VirtualizeService`), `Enhancer` pipeline, JVM CLI, JS façades. Re-exports `:gpx` via `api`, so `io.github.glandais.engine.path.*` and `…engine.gpx.*` stay importable from `:engine`. Also the module that links the standalone `.wasm` — see [`docs/wasm-wasi-abi.md`](docs/wasm-wasi-abi.md). | JVM, JS Node, JS browser, WASI |
|
|
47
|
+
| **`:fit`** | Garmin FIT encoding. `FitCourse` model, unit conversions and `FitEncoder` itself, all in commonMain over [`fit-kotlin-sdk`](https://github.com/glandais/fit-kotlin-sdk) — a multiplatform SDK generated from the FIT profile. One encoder, byte-identical output on every target, and no vendor SDK for consumers to install. | JVM, JS Node, JS browser, WASI |
|
|
47
48
|
| **`:map`** | Static map rendering: Web Mercator projection, image framing, tile download + cache, PNG output (`java.awt` / `ImageIO`). **JVM-only.** No default tile source — see [`map/README.md`](map/README.md) for the usage-policy obligations. | JVM only |
|
|
48
49
|
| **`:cli`** | Command-line tool (picocli). **JVM-only, not published as a library** — distributed as an executable jar. Replaces gpx2web's `gpxtools-cli`. | JVM only |
|
|
49
50
|
| **`:codegen`** | Tiny build-time helper that regenerates `GeneratedPath.kt` + `PointFieldAccessors.kt` from `PointField` (run only when the field list changes). | JVM only |
|
|
50
51
|
|
|
52
|
+
### Running it without a JVM or a JavaScript host
|
|
53
|
+
|
|
54
|
+
`:engine` also links a standalone **WASI module**, so the whole pipeline runs inside wasmtime,
|
|
55
|
+
WasmEdge, wazero, or an embedding in Go, Rust, Python or the JVM — no JavaScript involved.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
./gradlew :engine:wasmModule # -> engine/build/wasm/vcyclist-engine.wasm + .sha256
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
handle = exports["vcParseGpx"](store, len(gpx_bytes)) # after wiring three imports
|
|
63
|
+
print(exports["vcPathTotalDistance"](store, handle), "m")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The host provides `read_input`, `write_output` and `fetch_tile` (the last one may simply answer
|
|
67
|
+
"no tile"); everything else is numeric exports and integer handles.
|
|
68
|
+
[`docs/wasm-wasi-abi.md`](docs/wasm-wasi-abi.md) is the full contract, and
|
|
69
|
+
[`tools/wasi`](tools/wasi/README.md) is a working host that CI runs on every pull request.
|
|
70
|
+
|
|
51
71
|
### Migrating from gpx2web
|
|
52
72
|
|
|
53
73
|
vcyclist replaces the `gpx` and `gpxtools-cli` modules of
|
|
@@ -58,13 +78,11 @@ command-line options specifically, see [`cli/README.md`](cli/README.md).
|
|
|
58
78
|
|
|
59
79
|
## Install
|
|
60
80
|
|
|
61
|
-
### npm (Kotlin/JS
|
|
81
|
+
### npm (Kotlin/JS consumers)
|
|
62
82
|
|
|
63
83
|
```bash
|
|
64
84
|
npm install @glandais/vcyclist-engine # Kotlin/JS bundle
|
|
65
|
-
npm install @glandais/vcyclist-engine-wasm # Kotlin/Wasm bundle
|
|
66
85
|
npm install @glandais/vcyclist-elevation # Kotlin/JS bundle
|
|
67
|
-
npm install @glandais/vcyclist-elevation-wasm # Kotlin/Wasm bundle
|
|
68
86
|
```
|
|
69
87
|
|
|
70
88
|
### Gradle / Maven (JVM or KMP consumers)
|
|
@@ -72,13 +90,13 @@ npm install @glandais/vcyclist-elevation-wasm # Kotlin/Wasm bundle
|
|
|
72
90
|
```kotlin
|
|
73
91
|
// Gradle Kotlin DSL
|
|
74
92
|
dependencies {
|
|
75
|
-
implementation("io.github.glandais:vcyclist-engine:1.0.0") // pulls -jvm / -js
|
|
93
|
+
implementation("io.github.glandais:vcyclist-engine:1.0.0") // pulls -jvm / -js per target
|
|
76
94
|
implementation("io.github.glandais:vcyclist-elevation:1.0.0")
|
|
77
95
|
}
|
|
78
96
|
```
|
|
79
97
|
|
|
80
98
|
Replace `1.0.0` by the latest version shown in the badges above. KMP consumers automatically
|
|
81
|
-
get the platform-specific variant (`-jvm`, `-js
|
|
99
|
+
get the platform-specific variant (`-jvm`, `-js`) for their target.
|
|
82
100
|
`vcyclist-gpx` (the `Path` model + GPX I/O) comes in transitively via `vcyclist-engine`, and
|
|
83
101
|
can also be depended on alone if you only need parsing and resampling.
|
|
84
102
|
|
|
@@ -106,16 +124,14 @@ already have. Elevation correction is off unless you pass `--fix-elevation`, so
|
|
|
106
124
|
the network by default. Full usage, exit codes and the migration table from gpx2web's
|
|
107
125
|
`gpxtools-cli` are in [`cli/README.md`](cli/README.md).
|
|
108
126
|
|
|
109
|
-
### Try the browser
|
|
127
|
+
### Try the browser demo (elevation only)
|
|
110
128
|
|
|
111
129
|
```bash
|
|
112
|
-
# Kotlin/
|
|
113
|
-
./gradlew :elevation:wasmJsBrowserDevelopmentRun
|
|
114
|
-
# Kotlin/JS demo (sibling, same UI)
|
|
130
|
+
# Kotlin/JS demo
|
|
115
131
|
./gradlew :elevation:jsBrowserDevelopmentRun
|
|
116
132
|
```
|
|
117
133
|
|
|
118
|
-
|
|
134
|
+
The demo shares the [original TS demo](https://github.com/glandais/elevation) UI (Leaflet +
|
|
119
135
|
Chart.js + GPX upload). See [`elevation/README.md`](elevation/README.md) for details.
|
|
120
136
|
|
|
121
137
|
### Use from Kotlin
|
|
@@ -134,13 +150,68 @@ suspend fun virtualize(xml: String): String {
|
|
|
134
150
|
}
|
|
135
151
|
```
|
|
136
152
|
|
|
153
|
+
### Use from Java
|
|
154
|
+
|
|
155
|
+
The library is Kotlin-first, so its asynchronous entry points — elevation lookups and the
|
|
156
|
+
`Enhancer` pipeline that may call them — are `suspend` functions. From Java that would mean
|
|
157
|
+
hand-writing a `Continuation`, so each of them has a JVM bridge in two shapes:
|
|
158
|
+
|
|
159
|
+
| Shape | Suffix | Returns | For |
|
|
160
|
+
|---|---|---|---|
|
|
161
|
+
| Blocking | `…Blocking` | the value | batch jobs, CLIs, tests |
|
|
162
|
+
| Asynchronous | `…Async` | `CompletableFuture<T>` | servers, UIs |
|
|
163
|
+
|
|
164
|
+
```java
|
|
165
|
+
import io.github.glandais.elevation.ElevationProvider;
|
|
166
|
+
import io.github.glandais.elevation.ElevationProviderJvm;
|
|
167
|
+
import io.github.glandais.engine.EnhancerJvm;
|
|
168
|
+
import io.github.glandais.engine.gpx.GpxParserJvm;
|
|
169
|
+
import io.github.glandais.engine.gpx.GpxToPathKt;
|
|
170
|
+
import io.github.glandais.engine.gpx.GpxWriterJvm;
|
|
171
|
+
import io.github.glandais.engine.path.Path;
|
|
172
|
+
|
|
173
|
+
String xml = Files.readString(java.nio.file.Path.of("route.gpx"));
|
|
174
|
+
Path input = GpxToPathKt.firstTrackAsPath(GpxParserJvm.parse(xml));
|
|
175
|
+
|
|
176
|
+
// Physics only, nothing touches the network:
|
|
177
|
+
Path enhanced = EnhancerJvm.enhanceCourseDefaultBlocking(input);
|
|
178
|
+
|
|
179
|
+
// Or with elevation correction, off the calling thread:
|
|
180
|
+
ElevationProvider provider = ElevationProviderJvm.newElevationProvider();
|
|
181
|
+
CompletableFuture<Path> future = EnhancerJvm.enhanceCourseDefaultAsync(input, provider);
|
|
182
|
+
|
|
183
|
+
String out = GpxWriterJvm.write(enhanced);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Every entry point has a `…Jvm` twin**, in the same package as the Kotlin original:
|
|
187
|
+
`GpxParserJvm`, `GpxWriterJvm`, `GpxFromPathJvm`, `GpxModelJvm`, `PathSimplifierJvm`,
|
|
188
|
+
`ElevationStepJvm`, `TabularWritersJvm` (`:gpx`), `ElevationProviderJvm`, `TileFetcherJvm`
|
|
189
|
+
(`:elevation`), `EnhancerJvm`, `EngineModelJvm`, `ClimbDetectorJvm` (`:engine`), `PathToFitJvm`
|
|
190
|
+
(`:fit`), `MapFactoriesJvm` (`:map`). They exist for two reasons: Kotlin default arguments are
|
|
191
|
+
invisible to Java, and `@JvmOverloads` cannot be used on the common source sets where the API
|
|
192
|
+
lives. Call them and every optional parameter becomes optional again.
|
|
193
|
+
|
|
194
|
+
- `…Blocking` parks the calling thread. **Never call it from a UI thread, from inside a
|
|
195
|
+
coroutine, or from a thread of the executor you passed** — the first two freeze the caller,
|
|
196
|
+
the third can deadlock the pool. Exceptions propagate unchanged.
|
|
197
|
+
- `…Async` takes an optional `java.util.concurrent.Executor` (default: the coroutines IO
|
|
198
|
+
dispatcher, the right pool for network-bound work). Cancelling the returned future cancels the
|
|
199
|
+
work underneath; failures arrive as a `CompletionException`, so unwrap with `getCause()`.
|
|
200
|
+
- Everything else in the library is already synchronous and needs no bridge: `GpxParser`,
|
|
201
|
+
`GpxWriter`, `ElevationStep.smoothElevation`, `PathSimplifier`, the resamplers, FIT and the
|
|
202
|
+
CSV / JSON writers.
|
|
203
|
+
- `ElevationProviderJvm.newElevationProvider(config, fetcher)` takes a
|
|
204
|
+
`Function<String, RawTile>`, so a disk cache — or any other tile transport — plugs in from Java.
|
|
205
|
+
The fetcher may block (it runs on the IO dispatcher) but must be thread-safe: up to ten tiles
|
|
206
|
+
are fetched at once. See [`elevation/README.md`](elevation/README.md) for a complete example.
|
|
207
|
+
|
|
137
208
|
### Use from JavaScript / TypeScript
|
|
138
209
|
|
|
139
|
-
`generateTypeScriptDefinitions()` is enabled on
|
|
140
|
-
|
|
210
|
+
`generateTypeScriptDefinitions()` is enabled on `js(IR)`, so you get a `.d.ts` next to the
|
|
211
|
+
bundle in `build/dist/js/productionExecutable/vcyclist-engine.d.{ts,mts}`.
|
|
141
212
|
|
|
142
213
|
The Kotlin/JS variant (`@glandais/vcyclist-engine`, `@glandais/vcyclist-elevation`) runs
|
|
143
|
-
**in both browser and Node.js / Bun**.
|
|
214
|
+
**in both browser and Node.js / Bun**.
|
|
144
215
|
|
|
145
216
|
#### Getting at the exports
|
|
146
217
|
|
|
@@ -191,7 +262,7 @@ const { parseGpx, enhance, pathToFit } = engine;
|
|
|
191
262
|
|
|
192
263
|
const out = await enhance(parseGpx(gpxXml), null);
|
|
193
264
|
const fit = pathToFit(out, 'My route', Date.parse('2026-08-01T08:00:00Z'));
|
|
194
|
-
// Kotlin/JS returns an Int8Array
|
|
265
|
+
// Kotlin/JS returns an Int8Array containing the FIT file.
|
|
195
266
|
```
|
|
196
267
|
|
|
197
268
|
FIT has no relative clock, so the start instant is mandatory. The output is a **Course** file
|
|
@@ -205,7 +276,7 @@ several `<trk>` or several `<trkseg>` :
|
|
|
205
276
|
```js
|
|
206
277
|
const { parseGpxTracks, parseGpxSegments, writeGpxTracks } = engine;
|
|
207
278
|
|
|
208
|
-
const tracks = parseGpxTracks(gpxXml); // one path per <trk
|
|
279
|
+
const tracks = parseGpxTracks(gpxXml); // one path per <trk> *and* per <rte>
|
|
209
280
|
const segments = parseGpxSegments(gpxXml); // one path per <trkseg>, always continuous
|
|
210
281
|
const xml = writeGpxTracks(tracks); // one <trk> per path
|
|
211
282
|
```
|
|
@@ -213,6 +284,20 @@ const xml = writeGpxTracks(tracks); // one <trk> per path
|
|
|
213
284
|
Concatenating segments folds the pause between them into `totalDistance` — a `<trkseg>`
|
|
214
285
|
boundary is a physical discontinuity. Use `parseGpxSegments` when that artefact matters.
|
|
215
286
|
|
|
287
|
+
`parseGpxTracks` also returns `<rte>` routes, which many planners emit and which used to be
|
|
288
|
+
dropped silently. `parseGpxTracksOnly` and `parseGpxRoutesOnly` select one container or the other.
|
|
289
|
+
|
|
290
|
+
#### Options on the writers
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
const bare = engine.writeGpx(path, false); // no <extensions>: no power, heart rate, cadence
|
|
294
|
+
const fit = engine.pathsToFit([a, b], 'Two days', Date.parse('2026-08-01T08:00:00Z'));
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`writeGpx`, `writeGpxAt` and `writeGpxTracks` take a trailing `writeExtensions` flag (default
|
|
298
|
+
`true`). `pathsToFit` encodes several paths into one FIT course — a lap and a timer event pair
|
|
299
|
+
each — where `pathToFit` takes a single one.
|
|
300
|
+
|
|
216
301
|
On Node.js / Bun, tile decoding uses [`@jsquash/webp`](https://www.npmjs.com/package/@jsquash/webp)
|
|
217
302
|
(a pure-WASM WebP decoder, ~50 KB, listed as a runtime `dependency` of
|
|
218
303
|
`@glandais/vcyclist-engine` and `@glandais/vcyclist-elevation`). It is loaded lazily via
|
|
@@ -237,7 +322,7 @@ See [`demo/README.md`](demo/README.md) for the dev workflow and architecture.
|
|
|
237
322
|
|
|
238
323
|
```bash
|
|
239
324
|
./gradlew check # full build + all tests on all targets
|
|
240
|
-
./gradlew :engine:allTests # engine tests across JVM / JS Node / JS browser
|
|
325
|
+
./gradlew :engine:allTests # engine tests across JVM / JS Node / JS browser
|
|
241
326
|
./gradlew :elevation:allTests # elevation tests
|
|
242
327
|
./gradlew :elevation:jvmTest --tests '*Integration*' \
|
|
243
328
|
-PINTEGRATION=1 # live HTTP tests against tiles.mapterhorn.com
|
|
@@ -254,7 +339,7 @@ vcyclist/
|
|
|
254
339
|
│ ├── PLAN.md # task-by-task progress (Phases 1-2bis)
|
|
255
340
|
│ ├── parity.md # parity strategy vs the TS reference
|
|
256
341
|
│ ├── elevation-integration.md # how to run live HTTP integration tests
|
|
257
|
-
│ ├── kotlin-
|
|
342
|
+
│ ├── kotlin-js-jvm-webp.md # Kotlin/JS ↔ JS interop guide
|
|
258
343
|
│ └── tasks/ # one Markdown per implementation task (00-31, + bonus demos)
|
|
259
344
|
├── elevation/ # :elevation KMP module
|
|
260
345
|
├── engine/ # :engine KMP module (depends on :elevation)
|
|
@@ -273,11 +358,11 @@ vcyclist/
|
|
|
273
358
|
`TileFetcher.js.kt` (browser path unchanged, Node path uses `globalThis.fetch` +
|
|
274
359
|
`@jsquash/webp` WASM decoder loaded via lazy `eval('require')`), webpack externals to keep
|
|
275
360
|
the browser bundle free of `@jsquash/webp`, `ElevationProvider` auto-instantiation in
|
|
276
|
-
`EngineJsApi.enhance` when `opts.fixElevation` is true (JS
|
|
361
|
+
`EngineJsApi.enhance` when `opts.fixElevation` is true (JS façade), 6 jsTest classes
|
|
277
362
|
gated by `INTEGRATION=1`.
|
|
278
363
|
|
|
279
|
-
Total `:engine` test coverage : 32 test classes / ~326 commonTest cases /
|
|
280
|
-
~
|
|
364
|
+
Total `:engine` test coverage : 32 test classes / ~326 commonTest cases / 3 targets =
|
|
365
|
+
~1000 green executions, plus JVM-only smoke tests for the CLI and the full pipeline.
|
|
281
366
|
|
|
282
367
|
End-to-end smoke (after Phase 2bis) : sample.gpx (3569 source points, 130 km, ~4550 m gain)
|
|
283
368
|
runs through the complete `Enhancer` pipeline in ~1.7 s on JVM, producing ~1000 simplified
|
|
@@ -288,7 +373,7 @@ output points covering ~128.6 km / ~5.3 h of simulated ride.
|
|
|
288
373
|
- [`docs/PLAN.md`](docs/PLAN.md) — task-by-task plan with commit hashes for every step.
|
|
289
374
|
- [`docs/tasks/`](docs/tasks/) — detailed Markdown spec for each task (00-31 + bonus demos).
|
|
290
375
|
- [`docs/parity.md`](docs/parity.md) — TS↔Kotlin parity approach and tolerances.
|
|
291
|
-
- [`docs/kotlin-
|
|
376
|
+
- [`docs/kotlin-js-jvm-webp.md`](docs/kotlin-js-jvm-webp.md) — Kotlin/JS ↔ JS interop
|
|
292
377
|
guide that underpins the `@JsExport` façades and the WebP tile decoding.
|
|
293
378
|
- [`docs/publishing.md`](docs/publishing.md) — release flow (Maven Central + npm via
|
|
294
379
|
semantic-release on push to `develop`).
|