@glandais/vcyclist-engine 2.0.0 → 3.0.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 CHANGED
@@ -3,6 +3,7 @@
3
3
  [![npm engine](https://img.shields.io/npm/v/@glandais/vcyclist-engine?label=%40glandais%2Fvcyclist-engine)](https://www.npmjs.com/package/@glandais/vcyclist-engine)
4
4
  [![npm elevation](https://img.shields.io/npm/v/@glandais/vcyclist-elevation?label=%40glandais%2Fvcyclist-elevation)](https://www.npmjs.com/package/@glandais/vcyclist-elevation)
5
5
  [![Maven Central](https://img.shields.io/maven-central/v/io.github.glandais/vcyclist-engine?label=io.github.glandais%3Avcyclist-engine)](https://central.sonatype.com/artifact/io.github.glandais/vcyclist-engine)
6
+ [![Maven Central elevation](https://img.shields.io/maven-central/v/io.github.glandais/vcyclist-elevation?label=io.github.glandais%3Avcyclist-elevation)](https://central.sonatype.com/artifact/io.github.glandais/vcyclist-elevation)
6
7
  [![Maven Central gpx](https://img.shields.io/maven-central/v/io.github.glandais/vcyclist-gpx?label=io.github.glandais%3Avcyclist-gpx)](https://central.sonatype.com/artifact/io.github.glandais/vcyclist-gpx)
7
8
  [![Maven Central fit](https://img.shields.io/maven-central/v/io.github.glandais/vcyclist-fit?label=io.github.glandais%3Avcyclist-fit)](https://central.sonatype.com/artifact/io.github.glandais/vcyclist-fit)
8
9
  [![Maven Central map](https://img.shields.io/maven-central/v/io.github.glandais/vcyclist-map?label=io.github.glandais%3Avcyclist-map)](https://central.sonatype.com/artifact/io.github.glandais/vcyclist-map)
@@ -40,10 +41,10 @@ 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, Wasm 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, Wasm browser |
45
- | **`:engine`** | Physics (4 resistive `PowerProvider`s + cyclist input + `MaxSpeedComputer` + `VirtualizeService`), `Enhancer` pipeline, JVM CLI, JS/Wasm façades. Re-exports `:gpx` via `api`, so `io.github.glandais.engine.path.*` and `…engine.gpx.*` stay importable from `:engine`. | JVM, JS Node, JS browser, Wasm browser |
46
- | **`:fit`** | Garmin FIT encoding. `FitCourse` model + unit conversions in commonMain; `expect object FitEncoder` with a JVM `actual` on `com.garmin:fit` and JS/Wasm `actual`s on `@garmin/fitsdk`. | JVM, JS Node, JS browser, Wasm browser |
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 |
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 |
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`. | JVM, JS Node, JS browser |
47
+ | **`:fit`** | Garmin FIT encoding. `FitCourse` model + unit conversions in commonMain; `expect object FitEncoder` with a JVM `actual` on `com.garmin:fit` and a JS `actual` on `@garmin/fitsdk`. | JVM, JS Node, JS browser |
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 |
@@ -58,13 +59,11 @@ command-line options specifically, see [`cli/README.md`](cli/README.md).
58
59
 
59
60
  ## Install
60
61
 
61
- ### npm (Kotlin/JS or Kotlin/Wasm consumers)
62
+ ### npm (Kotlin/JS consumers)
62
63
 
63
64
  ```bash
64
65
  npm install @glandais/vcyclist-engine # Kotlin/JS bundle
65
- npm install @glandais/vcyclist-engine-wasm # Kotlin/Wasm bundle
66
66
  npm install @glandais/vcyclist-elevation # Kotlin/JS bundle
67
- npm install @glandais/vcyclist-elevation-wasm # Kotlin/Wasm bundle
68
67
  ```
69
68
 
70
69
  ### Gradle / Maven (JVM or KMP consumers)
@@ -72,13 +71,13 @@ npm install @glandais/vcyclist-elevation-wasm # Kotlin/Wasm bundle
72
71
  ```kotlin
73
72
  // Gradle Kotlin DSL
74
73
  dependencies {
75
- implementation("io.github.glandais:vcyclist-engine:1.0.0") // pulls -jvm / -js / -wasm-js per target
74
+ implementation("io.github.glandais:vcyclist-engine:1.0.0") // pulls -jvm / -js per target
76
75
  implementation("io.github.glandais:vcyclist-elevation:1.0.0")
77
76
  }
78
77
  ```
79
78
 
80
79
  Replace `1.0.0` by the latest version shown in the badges above. KMP consumers automatically
81
- get the platform-specific variant (`-jvm`, `-js`, `-wasm-js`) for their target.
80
+ get the platform-specific variant (`-jvm`, `-js`) for their target.
82
81
  `vcyclist-gpx` (the `Path` model + GPX I/O) comes in transitively via `vcyclist-engine`, and
83
82
  can also be depended on alone if you only need parsing and resampling.
84
83
 
@@ -106,16 +105,14 @@ already have. Elevation correction is off unless you pass `--fix-elevation`, so
106
105
  the network by default. Full usage, exit codes and the migration table from gpx2web's
107
106
  `gpxtools-cli` are in [`cli/README.md`](cli/README.md).
108
107
 
109
- ### Try the browser demos (elevation only)
108
+ ### Try the browser demo (elevation only)
110
109
 
111
110
  ```bash
112
- # Kotlin/Wasm demo
113
- ./gradlew :elevation:wasmJsBrowserDevelopmentRun
114
- # Kotlin/JS demo (sibling, same UI)
111
+ # Kotlin/JS demo
115
112
  ./gradlew :elevation:jsBrowserDevelopmentRun
116
113
  ```
117
114
 
118
- Both demos share the [original TS demo](https://github.com/glandais/elevation) UI (Leaflet +
115
+ The demo shares the [original TS demo](https://github.com/glandais/elevation) UI (Leaflet +
119
116
  Chart.js + GPX upload). See [`elevation/README.md`](elevation/README.md) for details.
120
117
 
121
118
  ### Use from Kotlin
@@ -134,13 +131,68 @@ suspend fun virtualize(xml: String): String {
134
131
  }
135
132
  ```
136
133
 
134
+ ### Use from Java
135
+
136
+ The library is Kotlin-first, so its asynchronous entry points — elevation lookups and the
137
+ `Enhancer` pipeline that may call them — are `suspend` functions. From Java that would mean
138
+ hand-writing a `Continuation`, so each of them has a JVM bridge in two shapes:
139
+
140
+ | Shape | Suffix | Returns | For |
141
+ |---|---|---|---|
142
+ | Blocking | `…Blocking` | the value | batch jobs, CLIs, tests |
143
+ | Asynchronous | `…Async` | `CompletableFuture<T>` | servers, UIs |
144
+
145
+ ```java
146
+ import io.github.glandais.elevation.ElevationProvider;
147
+ import io.github.glandais.elevation.ElevationProviderJvm;
148
+ import io.github.glandais.engine.EnhancerJvm;
149
+ import io.github.glandais.engine.gpx.GpxParserJvm;
150
+ import io.github.glandais.engine.gpx.GpxToPathKt;
151
+ import io.github.glandais.engine.gpx.GpxWriterJvm;
152
+ import io.github.glandais.engine.path.Path;
153
+
154
+ String xml = Files.readString(java.nio.file.Path.of("route.gpx"));
155
+ Path input = GpxToPathKt.firstTrackAsPath(GpxParserJvm.parse(xml));
156
+
157
+ // Physics only, nothing touches the network:
158
+ Path enhanced = EnhancerJvm.enhanceCourseDefaultBlocking(input);
159
+
160
+ // Or with elevation correction, off the calling thread:
161
+ ElevationProvider provider = ElevationProviderJvm.newElevationProvider();
162
+ CompletableFuture<Path> future = EnhancerJvm.enhanceCourseDefaultAsync(input, provider);
163
+
164
+ String out = GpxWriterJvm.write(enhanced);
165
+ ```
166
+
167
+ **Every entry point has a `…Jvm` twin**, in the same package as the Kotlin original:
168
+ `GpxParserJvm`, `GpxWriterJvm`, `GpxFromPathJvm`, `GpxModelJvm`, `PathSimplifierJvm`,
169
+ `ElevationStepJvm`, `TabularWritersJvm` (`:gpx`), `ElevationProviderJvm`, `TileFetcherJvm`
170
+ (`:elevation`), `EnhancerJvm`, `EngineModelJvm`, `ClimbDetectorJvm` (`:engine`), `PathToFitJvm`
171
+ (`:fit`), `MapFactoriesJvm` (`:map`). They exist for two reasons: Kotlin default arguments are
172
+ invisible to Java, and `@JvmOverloads` cannot be used on the common source sets where the API
173
+ lives. Call them and every optional parameter becomes optional again.
174
+
175
+ - `…Blocking` parks the calling thread. **Never call it from a UI thread, from inside a
176
+ coroutine, or from a thread of the executor you passed** — the first two freeze the caller,
177
+ the third can deadlock the pool. Exceptions propagate unchanged.
178
+ - `…Async` takes an optional `java.util.concurrent.Executor` (default: the coroutines IO
179
+ dispatcher, the right pool for network-bound work). Cancelling the returned future cancels the
180
+ work underneath; failures arrive as a `CompletionException`, so unwrap with `getCause()`.
181
+ - Everything else in the library is already synchronous and needs no bridge: `GpxParser`,
182
+ `GpxWriter`, `ElevationStep.smoothElevation`, `PathSimplifier`, the resamplers, FIT and the
183
+ CSV / JSON writers.
184
+ - `ElevationProviderJvm.newElevationProvider(config, fetcher)` takes a
185
+ `Function<String, RawTile>`, so a disk cache — or any other tile transport — plugs in from Java.
186
+ The fetcher may block (it runs on the IO dispatcher) but must be thread-safe: up to ten tiles
187
+ are fetched at once. See [`elevation/README.md`](elevation/README.md) for a complete example.
188
+
137
189
  ### Use from JavaScript / TypeScript
138
190
 
139
- `generateTypeScriptDefinitions()` is enabled on both `js(IR)` and `wasmJs`, so you get a
140
- `.d.ts` next to the bundle in `build/dist/{js,wasmJs}/productionExecutable/vcyclist-engine.d.{ts,mts}`.
191
+ `generateTypeScriptDefinitions()` is enabled on `js(IR)`, so you get a `.d.ts` next to the
192
+ bundle in `build/dist/js/productionExecutable/vcyclist-engine.d.{ts,mts}`.
141
193
 
142
194
  The Kotlin/JS variant (`@glandais/vcyclist-engine`, `@glandais/vcyclist-elevation`) runs
143
- **in both browser and Node.js / Bun**. The Wasm variants (`*-wasm`) are browser-only.
195
+ **in both browser and Node.js / Bun**.
144
196
 
145
197
  #### Getting at the exports
146
198
 
@@ -191,7 +243,7 @@ const { parseGpx, enhance, pathToFit } = engine;
191
243
 
192
244
  const out = await enhance(parseGpx(gpxXml), null);
193
245
  const fit = pathToFit(out, 'My route', Date.parse('2026-08-01T08:00:00Z'));
194
- // Kotlin/JS returns an Int8Array; the -wasm build returns a Uint8Array. Both are the same file.
246
+ // Kotlin/JS returns an Int8Array containing the FIT file.
195
247
  ```
196
248
 
197
249
  FIT has no relative clock, so the start instant is mandatory. The output is a **Course** file
@@ -205,7 +257,7 @@ several `<trk>` or several `<trkseg>` :
205
257
  ```js
206
258
  const { parseGpxTracks, parseGpxSegments, writeGpxTracks } = engine;
207
259
 
208
- const tracks = parseGpxTracks(gpxXml); // one path per <trk>, segments concatenated
260
+ const tracks = parseGpxTracks(gpxXml); // one path per <trk> *and* per <rte>
209
261
  const segments = parseGpxSegments(gpxXml); // one path per <trkseg>, always continuous
210
262
  const xml = writeGpxTracks(tracks); // one <trk> per path
211
263
  ```
@@ -213,6 +265,20 @@ const xml = writeGpxTracks(tracks); // one <trk> per path
213
265
  Concatenating segments folds the pause between them into `totalDistance` — a `<trkseg>`
214
266
  boundary is a physical discontinuity. Use `parseGpxSegments` when that artefact matters.
215
267
 
268
+ `parseGpxTracks` also returns `<rte>` routes, which many planners emit and which used to be
269
+ dropped silently. `parseGpxTracksOnly` and `parseGpxRoutesOnly` select one container or the other.
270
+
271
+ #### Options on the writers
272
+
273
+ ```js
274
+ const bare = engine.writeGpx(path, false); // no <extensions>: no power, heart rate, cadence
275
+ const fit = engine.pathsToFit([a, b], 'Two days', Date.parse('2026-08-01T08:00:00Z'));
276
+ ```
277
+
278
+ `writeGpx`, `writeGpxAt` and `writeGpxTracks` take a trailing `writeExtensions` flag (default
279
+ `true`). `pathsToFit` encodes several paths into one FIT course — a lap and a timer event pair
280
+ each — where `pathToFit` takes a single one.
281
+
216
282
  On Node.js / Bun, tile decoding uses [`@jsquash/webp`](https://www.npmjs.com/package/@jsquash/webp)
217
283
  (a pure-WASM WebP decoder, ~50 KB, listed as a runtime `dependency` of
218
284
  `@glandais/vcyclist-engine` and `@glandais/vcyclist-elevation`). It is loaded lazily via
@@ -237,7 +303,7 @@ See [`demo/README.md`](demo/README.md) for the dev workflow and architecture.
237
303
 
238
304
  ```bash
239
305
  ./gradlew check # full build + all tests on all targets
240
- ./gradlew :engine:allTests # engine tests across JVM / JS Node / JS browser / Wasm browser
306
+ ./gradlew :engine:allTests # engine tests across JVM / JS Node / JS browser
241
307
  ./gradlew :elevation:allTests # elevation tests
242
308
  ./gradlew :elevation:jvmTest --tests '*Integration*' \
243
309
  -PINTEGRATION=1 # live HTTP tests against tiles.mapterhorn.com
@@ -254,7 +320,7 @@ vcyclist/
254
320
  │ ├── PLAN.md # task-by-task progress (Phases 1-2bis)
255
321
  │ ├── parity.md # parity strategy vs the TS reference
256
322
  │ ├── elevation-integration.md # how to run live HTTP integration tests
257
- │ ├── kotlin-wasm-jvm-webp.md # Kotlin/Wasm ↔ JS interop guide
323
+ │ ├── kotlin-js-jvm-webp.md # Kotlin/JS ↔ JS interop guide
258
324
  │ └── tasks/ # one Markdown per implementation task (00-31, + bonus demos)
259
325
  ├── elevation/ # :elevation KMP module
260
326
  ├── engine/ # :engine KMP module (depends on :elevation)
@@ -273,11 +339,11 @@ vcyclist/
273
339
  `TileFetcher.js.kt` (browser path unchanged, Node path uses `globalThis.fetch` +
274
340
  `@jsquash/webp` WASM decoder loaded via lazy `eval('require')`), webpack externals to keep
275
341
  the browser bundle free of `@jsquash/webp`, `ElevationProvider` auto-instantiation in
276
- `EngineJsApi.enhance` when `opts.fixElevation` is true (JS + Wasm façades), 6 jsTest classes
342
+ `EngineJsApi.enhance` when `opts.fixElevation` is true (JS façade), 6 jsTest classes
277
343
  gated by `INTEGRATION=1`.
278
344
 
279
- Total `:engine` test coverage : 32 test classes / ~326 commonTest cases / 4 targets =
280
- ~1300 green executions, plus JVM-only smoke tests for the CLI and the full pipeline.
345
+ Total `:engine` test coverage : 32 test classes / ~326 commonTest cases / 3 targets =
346
+ ~1000 green executions, plus JVM-only smoke tests for the CLI and the full pipeline.
281
347
 
282
348
  End-to-end smoke (after Phase 2bis) : sample.gpx (3569 source points, 130 km, ~4550 m gain)
283
349
  runs through the complete `Enhancer` pipeline in ~1.7 s on JVM, producing ~1000 simplified
@@ -288,7 +354,7 @@ output points covering ~128.6 km / ~5.3 h of simulated ride.
288
354
  - [`docs/PLAN.md`](docs/PLAN.md) — task-by-task plan with commit hashes for every step.
289
355
  - [`docs/tasks/`](docs/tasks/) — detailed Markdown spec for each task (00-31 + bonus demos).
290
356
  - [`docs/parity.md`](docs/parity.md) — TS↔Kotlin parity approach and tolerances.
291
- - [`docs/kotlin-wasm-jvm-webp.md`](docs/kotlin-wasm-jvm-webp.md) — Kotlin/Wasm ↔ JS interop
357
+ - [`docs/kotlin-js-jvm-webp.md`](docs/kotlin-js-jvm-webp.md) — Kotlin/JS ↔ JS interop
292
358
  guide that underpins the `@JsExport` façades and the WebP tile decoding.
293
359
  - [`docs/publishing.md`](docs/publishing.md) — release flow (Maven Central + npm via
294
360
  semantic-release on push to `develop`).