@glandais/vcyclist-engine 4.2.1 → 5.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 +141 -276
- package/index.cjs +70 -0
- package/index.d.ts +725 -0
- package/index.mjs +70 -0
- package/kotlin-kotlin-stdlib.js +41 -41
- package/kotlin-kotlin-stdlib.js.map +1 -1
- package/package.json +11 -3
- package/vcyclist-elevation.js +160 -44
- package/vcyclist-elevation.js.map +1 -1
- package/vcyclist-engine.d.ts +10 -6
- package/vcyclist-engine.js +455 -255
- package/vcyclist-engine.js.map +1 -1
- package/vcyclist-fit.js +2 -2
- package/vcyclist-fit.js.map +1 -1
- package/vcyclist-gpx.js +748 -275
- package/vcyclist-gpx.js.map +1 -1
package/README.md
CHANGED
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
[](https://central.sonatype.com/artifact/io.github.glandais/vcyclist-fit)
|
|
9
9
|
[](https://central.sonatype.com/artifact/io.github.glandais/vcyclist-map)
|
|
10
10
|
|
|
11
|
-
Kotlin Multiplatform
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
TypeScript library for elevation data.
|
|
11
|
+
Kotlin Multiplatform physics-based cycling simulator: it turns a static GPS trace into a
|
|
12
|
+
virtualized ride with realistic speeds, times and power estimates. Elevation data comes from
|
|
13
|
+
Terrarium-encoded DEM tiles — mapterhorn by default — fetched and decoded by the `:elevation`
|
|
14
|
+
module.
|
|
16
15
|
|
|
17
16
|
```
|
|
18
17
|
┌──────────────┐
|
|
@@ -31,7 +30,8 @@ TypeScript library for elevation data.
|
|
|
31
30
|
│ ├─ VirtualizeService (1 Hz physics) │
|
|
32
31
|
│ ├─ PointPerSecond (uniform sampling) │
|
|
33
32
|
│ ├─ W′bal (Critical Power annotation) │
|
|
34
|
-
│
|
|
33
|
+
│ ├─ PathSimplifier (Douglas-Peucker 3D) │
|
|
34
|
+
│ └─ ElevationGain (D+/D− dead band) │
|
|
35
35
|
└──────────────────┬──────────────────────┘
|
|
36
36
|
▼
|
|
37
37
|
┌──────────────┐
|
|
@@ -40,358 +40,223 @@ TypeScript library for elevation data.
|
|
|
40
40
|
(*) optional — needs an ElevationProvider
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
##
|
|
43
|
+
## What it can do
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|---|---|---|
|
|
47
|
-
| **`: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 |
|
|
48
|
-
| **`:gpx`** | Path model (38 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 |
|
|
49
|
-
| **`: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 |
|
|
50
|
-
| **`: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 |
|
|
51
|
-
| **`: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 |
|
|
52
|
-
| **`:cli`** | Command-line tool (picocli). **JVM-only, not published as a library** — distributed as an executable jar. Replaces gpx2web's `gpxtools-cli`. | JVM only |
|
|
53
|
-
| **`:codegen`** | Tiny build-time helper that regenerates `GeneratedPath.kt` + `PointFieldAccessors.kt` from `PointField` (run only when the field list changes). | JVM only |
|
|
45
|
+
**In**: GPX tracks, routes (`<rte>`), segments and waypoints.
|
|
54
46
|
|
|
55
|
-
|
|
47
|
+
**Physics**: elevation correction from DEM tiles, curvature estimation or an optimal racing line,
|
|
48
|
+
cornering and braking sharing one friction-ellipse budget, a configurable rider and bike, and a
|
|
49
|
+
1 Hz time-stepping simulation that produces speed, time and power at every point.
|
|
56
50
|
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
**Out**: GPX, Garmin FIT courses, CSV, column-oriented JSON, static PNG maps — plus climb
|
|
52
|
+
detection, a racing-line report and worst-case wind analysis.
|
|
59
53
|
|
|
60
|
-
|
|
61
|
-
./gradlew :engine:wasmModule # -> engine/build/wasm/vcyclist-engine.wasm + .sha256
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
```python
|
|
65
|
-
handle = exports["vcParseGpx"](store, len(gpx_bytes)) # after wiring three imports
|
|
66
|
-
print(exports["vcPathTotalDistance"](store, handle), "m")
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
The host provides `read_input`, `write_output` and `fetch_tile` (the last one may simply answer
|
|
70
|
-
"no tile"); everything else is numeric exports and integer handles.
|
|
71
|
-
[`docs/wasm-wasi-abi.md`](docs/wasm-wasi-abi.md) is the full contract, and
|
|
72
|
-
[`tools/wasi`](tools/wasi/README.md) is a working host that CI runs on every pull request.
|
|
54
|
+
Four doors reach the same engine, and a capability is available from all of them unless noted:
|
|
73
55
|
|
|
74
|
-
|
|
56
|
+
| Capability | CLI | Kotlin / Java | JavaScript / TS | WASI |
|
|
57
|
+
|---|---|---|---|---|
|
|
58
|
+
| Run the pipeline | `enhance` | `Enhancer.enhanceCourseDefault` | `enhance` | `vcEnhance` |
|
|
59
|
+
| Configure rider, bike, wind, power | `--cyclist-*`, `--bike-*`, `--wind-*` | `CoursePhysics(Course(…))` | `enhanceWithCourse` | `vcEnhanceWithCourse` |
|
|
60
|
+
| Power models `constant` · `durability` · `critical-power` · `from_data` | `--cyclist-model` | `CyclistPowerSpec` | `power.type` | `power.type` |
|
|
61
|
+
| Terrain pacing, power slew limit | `--cyclist-pacing`, `--cyclist-slew` | provider decorators | `power.pacing`, `power.maxSlewWPerS` | idem |
|
|
62
|
+
| Road condition, dry or wet | `--road-condition` | `Cyclist.withRoadCondition` | `cyclist.roadCondition` | idem |
|
|
63
|
+
| Pedal-strike clearance | `--bike-max-pedal-angle` | `Bike.maxPedalingLeanAngleDeg` | `bike.maxPedalingLeanAngleDeg` | idem |
|
|
64
|
+
| Racing line + corridor mode | `--racing-line`, `--corridor` | `RacingLineOptions` | `racingLineEnabled` | idem |
|
|
65
|
+
| DEM elevation correction | `--fix-elevation` | `ElevationProvider` | `fixElevation: true` | host serves tiles |
|
|
66
|
+
| Climb detection | — | `ClimbDetector.detect` | `detectClimbs` | `vcDetectClimbsJson` |
|
|
67
|
+
| Racing-line report | `--racing-line-report` | `RacingLine.analyze` | `analyzeRacingLine` | `vcAnalyzeRacingLineJson` |
|
|
68
|
+
| Write GPX / CSV / JSON | `--gpx --csv --json` | `GpxWriter`, `CsvWriter`, `JsonWriter` | `writeGpx`, `pathToCsv`, `pathToJson` | `vcWriteGpx`, `vcPathToCsv/Json` |
|
|
69
|
+
| Write FIT course | `--fit` | `Path.toFitBytes` | `pathToFit`, `pathsToFit` | `vcPathToFit`, `vcPathsToFit` |
|
|
70
|
+
| Static map PNG | `export --map` | `MapFactoriesJvm` | — | — |
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
[
|
|
78
|
-
|
|
79
|
-
replaced, or not ported with the reason — plus the deliberate behavioural differences. For the
|
|
80
|
-
command-line options specifically, see [`cli/README.md`](cli/README.md).
|
|
72
|
+
Static maps are JVM-only by construction — `:map` draws on `java.awt`.
|
|
73
|
+
[`docs/ledgers/surface-coverage.md`](docs/ledgers/surface-coverage.md) tracks this matrix as
|
|
74
|
+
capabilities land, so that a feature cannot reach one door and quietly miss the others.
|
|
81
75
|
|
|
82
76
|
## Install
|
|
83
77
|
|
|
84
|
-
### npm
|
|
78
|
+
### npm — browser, Node.js and Bun
|
|
85
79
|
|
|
86
80
|
```bash
|
|
87
|
-
npm install @glandais/vcyclist-engine #
|
|
88
|
-
npm install @glandais/vcyclist-elevation #
|
|
81
|
+
npm install @glandais/vcyclist-engine # physics, GPX, FIT, CSV/JSON, climbs, racing line
|
|
82
|
+
npm install @glandais/vcyclist-elevation # DEM lookups on their own
|
|
89
83
|
```
|
|
90
84
|
|
|
91
|
-
|
|
85
|
+
The engine bundle already carries the elevation façade; install the second package only if you
|
|
86
|
+
want DEM lookups without the physics.
|
|
87
|
+
|
|
88
|
+
### Gradle — JVM or Kotlin Multiplatform
|
|
92
89
|
|
|
93
90
|
```kotlin
|
|
94
|
-
// Gradle Kotlin DSL
|
|
95
91
|
dependencies {
|
|
96
|
-
implementation("io.github.glandais:vcyclist-engine:
|
|
97
|
-
implementation("io.github.glandais:vcyclist-elevation:
|
|
92
|
+
implementation("io.github.glandais:vcyclist-engine:4.2.1") // pulls -jvm / -js per target
|
|
93
|
+
implementation("io.github.glandais:vcyclist-elevation:4.2.1")
|
|
98
94
|
}
|
|
99
95
|
```
|
|
100
96
|
|
|
101
|
-
|
|
102
|
-
get the platform-specific variant (`-jvm`, `-js`) for their target.
|
|
103
|
-
`vcyclist-gpx` (the `Path` model + GPX I/O) comes in transitively via `vcyclist-engine`, and
|
|
104
|
-
can also be depended on alone if you only need parsing and resampling.
|
|
97
|
+
### Maven
|
|
105
98
|
|
|
106
|
-
|
|
99
|
+
```xml
|
|
100
|
+
<dependency>
|
|
101
|
+
<groupId>io.github.glandais</groupId>
|
|
102
|
+
<artifactId>vcyclist-engine-jvm</artifactId>
|
|
103
|
+
<version>4.2.1</version>
|
|
104
|
+
</dependency>
|
|
105
|
+
```
|
|
107
106
|
|
|
108
|
-
|
|
107
|
+
The badges above are the source of truth for the current version. KMP consumers get the
|
|
108
|
+
platform-specific variant automatically; from plain Maven, name the `-jvm` artifact yourself.
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
`vcyclist-gpx` (the `Path` model + GPX I/O) comes in transitively via `vcyclist-engine`, and can be
|
|
111
|
+
depended on alone if you only need parsing and resampling. `vcyclist-fit` and `vcyclist-map` are
|
|
112
|
+
published separately.
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
attached to each [GitHub release](https://github.com/glandais/vcyclist/releases). Download
|
|
114
|
-
`vcyclist-cli-<version>-all.jar` and run it with Java 21+, or build it yourself:
|
|
114
|
+
### CLI and `.wasm`
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
java -jar cli/build/libs/vcyclist-cli-*-all.jar enhance route.gpx --gpx out.gpx --csv out.csv
|
|
116
|
+
The CLI is an application, not a library, so it is **not** on Maven Central: download
|
|
117
|
+
`vcyclist-cli-<version>-all.jar` from a [GitHub release](https://github.com/glandais/vcyclist/releases).
|
|
118
|
+
The WASI module is built from source (below) rather than published to a registry.
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
**Requirements**: Java 21+ for the JVM and CLI; Node ≥ 18 (22+ recommended) or Bun for JavaScript;
|
|
121
|
+
a WASI runtime with the `function-references`, `gc` and `exceptions` proposals — wasmtime 46+ is
|
|
122
|
+
known good.
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
already have. Elevation correction is off unless you pass `--fix-elevation`, so nothing touches
|
|
127
|
-
the network by default. Full usage, exit codes and the migration table from gpx2web's
|
|
128
|
-
`gpxtools-cli` are in [`cli/README.md`](cli/README.md).
|
|
124
|
+
## Quick start
|
|
129
125
|
|
|
130
|
-
###
|
|
126
|
+
### Command line
|
|
131
127
|
|
|
132
128
|
```bash
|
|
133
|
-
|
|
134
|
-
./gradlew :elevation:jsBrowserDevelopmentRun
|
|
129
|
+
java -jar vcyclist-cli-*-all.jar enhance route.gpx --gpx out.gpx --csv out.csv
|
|
135
130
|
```
|
|
136
131
|
|
|
137
|
-
|
|
138
|
-
|
|
132
|
+
`enhance` runs the physics pipeline; `export` produces maps, FIT, CSV and JSON from a file you
|
|
133
|
+
already have. Elevation correction is off unless you pass `--fix-elevation`, so nothing touches the
|
|
134
|
+
network by default.
|
|
135
|
+
|
|
136
|
+
Full option reference, the rider models and what each is measured to be worth, and exit codes:
|
|
137
|
+
[`cli/README.md`](cli/README.md).
|
|
139
138
|
|
|
140
|
-
###
|
|
139
|
+
### Kotlin
|
|
141
140
|
|
|
142
141
|
```kotlin
|
|
143
142
|
import io.github.glandais.engine.Enhancer
|
|
144
143
|
import io.github.glandais.engine.gpx.GpxParser
|
|
144
|
+
import io.github.glandais.engine.gpx.GpxWriter
|
|
145
145
|
import io.github.glandais.engine.gpx.firstTrackAsPath
|
|
146
|
+
import io.github.glandais.engine.gpx.toGpxDocument
|
|
146
147
|
|
|
147
148
|
suspend fun virtualize(xml: String): String {
|
|
148
149
|
val path = GpxParser.parse(xml).firstTrackAsPath()
|
|
149
|
-
val out = Enhancer.enhanceCourseDefault(path)
|
|
150
|
-
return
|
|
151
|
-
out.toGpxDocument(trackName = "virtualized")
|
|
152
|
-
)
|
|
150
|
+
val out = Enhancer.enhanceCourseDefault(path) // pure physics, no HTTP
|
|
151
|
+
return GpxWriter.write(out.toGpxDocument(trackName = "virtualized"))
|
|
153
152
|
}
|
|
154
153
|
```
|
|
155
154
|
|
|
156
|
-
|
|
155
|
+
Pass an `ElevationProvider` as the second argument to correct elevations from DEM tiles, and an
|
|
156
|
+
`EnhanceOptions` as the third to configure the pipeline. For a configured rider, build a
|
|
157
|
+
`CoursePhysics(Course(path, cyclist, bike), …)` and call `Enhancer.enhanceCourse`.
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
`Enhancer` pipeline that may call them — are `suspend` functions. From Java that would mean
|
|
160
|
-
hand-writing a `Continuation`, so each of them has a JVM bridge in two shapes:
|
|
161
|
-
|
|
162
|
-
| Shape | Suffix | Returns | For |
|
|
163
|
-
|---|---|---|---|
|
|
164
|
-
| Blocking | `…Blocking` | the value | batch jobs, CLIs, tests |
|
|
165
|
-
| Asynchronous | `…Async` | `CompletableFuture<T>` | servers, UIs |
|
|
159
|
+
### Java
|
|
166
160
|
|
|
167
161
|
```java
|
|
168
|
-
|
|
169
|
-
import io.github.glandais.elevation.ElevationProviderJvm;
|
|
170
|
-
import io.github.glandais.engine.EnhancerJvm;
|
|
171
|
-
import io.github.glandais.engine.gpx.GpxParserJvm;
|
|
172
|
-
import io.github.glandais.engine.gpx.GpxToPathKt;
|
|
173
|
-
import io.github.glandais.engine.gpx.GpxWriterJvm;
|
|
174
|
-
import io.github.glandais.engine.path.Path;
|
|
175
|
-
|
|
176
|
-
String xml = Files.readString(java.nio.file.Path.of("route.gpx"));
|
|
177
|
-
Path input = GpxToPathKt.firstTrackAsPath(GpxParserJvm.parse(xml));
|
|
178
|
-
|
|
179
|
-
// Physics only, nothing touches the network:
|
|
162
|
+
Path input = GpxToPathJvm.firstTrackAsPath(GpxParserJvm.parse(xml));
|
|
180
163
|
Path enhanced = EnhancerJvm.enhanceCourseDefaultBlocking(input);
|
|
181
|
-
|
|
182
|
-
// Or with elevation correction, off the calling thread:
|
|
183
|
-
ElevationProvider provider = ElevationProviderJvm.newElevationProvider();
|
|
184
|
-
CompletableFuture<Path> future = EnhancerJvm.enhanceCourseDefaultAsync(input, provider);
|
|
185
|
-
|
|
186
164
|
String out = GpxWriterJvm.write(enhanced);
|
|
187
165
|
```
|
|
188
166
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
`
|
|
192
|
-
|
|
193
|
-
(`:fit`), `MapFactoriesJvm` (`:map`). They exist for two reasons: Kotlin default arguments are
|
|
194
|
-
invisible to Java, and `@JvmOverloads` cannot be used on the common source sets where the API
|
|
195
|
-
lives. Call them and every optional parameter becomes optional again.
|
|
196
|
-
|
|
197
|
-
- `…Blocking` parks the calling thread. **Never call it from a UI thread, from inside a
|
|
198
|
-
coroutine, or from a thread of the executor you passed** — the first two freeze the caller,
|
|
199
|
-
the third can deadlock the pool. Exceptions propagate unchanged.
|
|
200
|
-
- `…Async` takes an optional `java.util.concurrent.Executor` (default: the coroutines IO
|
|
201
|
-
dispatcher, the right pool for network-bound work). Cancelling the returned future cancels the
|
|
202
|
-
work underneath; failures arrive as a `CompletionException`, so unwrap with `getCause()`.
|
|
203
|
-
- Everything else in the library is already synchronous and needs no bridge: `GpxParser`,
|
|
204
|
-
`GpxWriter`, `ElevationStep.smoothElevation`, `PathSimplifier`, the resamplers, FIT and the
|
|
205
|
-
CSV / JSON writers.
|
|
206
|
-
- `ElevationProviderJvm.newElevationProvider(config, fetcher)` takes a
|
|
207
|
-
`Function<String, RawTile>`, so a disk cache — or any other tile transport — plugs in from Java.
|
|
208
|
-
The fetcher may block (it runs on the IO dispatcher) but must be thread-safe: up to ten tiles
|
|
209
|
-
are fetched at once. See [`elevation/README.md`](elevation/README.md) for a complete example.
|
|
210
|
-
|
|
211
|
-
### Use from JavaScript / TypeScript
|
|
212
|
-
|
|
213
|
-
`generateTypeScriptDefinitions()` is enabled on `js(IR)`, so you get a `.d.ts` next to the
|
|
214
|
-
bundle in `build/dist/js/productionExecutable/vcyclist-engine.d.{ts,mts}`.
|
|
215
|
-
|
|
216
|
-
The Kotlin/JS variant (`@glandais/vcyclist-engine`, `@glandais/vcyclist-elevation`) runs
|
|
217
|
-
**in both browser and Node.js / Bun**.
|
|
218
|
-
|
|
219
|
-
#### Getting at the exports
|
|
220
|
-
|
|
221
|
-
Kotlin/JS emits a **UMD bundle that preserves the package namespace**, so the module has
|
|
222
|
-
exactly one top-level export, `io`. Named imports do not work — `import { parseGpx } from
|
|
223
|
-
'@glandais/vcyclist-engine'` fails under Node ESM with *"Named export 'parseGpx' not found"*.
|
|
224
|
-
Unwrap the namespace once and destructure from it :
|
|
225
|
-
|
|
226
|
-
```js
|
|
227
|
-
import * as engineRaw from '@glandais/vcyclist-engine'; // or: require('@glandais/vcyclist-engine')
|
|
228
|
-
|
|
229
|
-
const engine = engineRaw.io.github.glandais.engine;
|
|
230
|
-
```
|
|
167
|
+
Every entry point has a `…Jvm` twin that restores Kotlin's default arguments, and every `suspend`
|
|
168
|
+
function has both a `…Blocking` and a `…Async` (`CompletableFuture`) bridge.
|
|
169
|
+
**[`docs/guides/using-from-java.md`](docs/guides/using-from-java.md)** has the calling rules — some
|
|
170
|
+
of them matter, `…Blocking` on a UI thread being the obvious one.
|
|
231
171
|
|
|
232
|
-
|
|
233
|
-
adds TypeScript types for the DTOs, which Kotlin/JS emits as referenced names only.
|
|
172
|
+
### JavaScript / TypeScript
|
|
234
173
|
|
|
235
|
-
|
|
174
|
+
Kotlin/JS emits a UMD bundle that preserves the package namespace, so there is exactly one
|
|
175
|
+
top-level export. Named imports do not work — unwrap it once:
|
|
236
176
|
|
|
237
177
|
```js
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const handle = parseGpx(gpxXml);
|
|
241
|
-
console.log('input points:', pathSize(handle));
|
|
242
|
-
const out = await enhance(handle, null); // physics only, no HTTP
|
|
243
|
-
console.log('output:', pathSize(out), pathTotalDistance(out), 'm');
|
|
244
|
-
const xml = writeGpx(out);
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
#### Node.js / Bun (with elevation correction)
|
|
248
|
-
|
|
249
|
-
```js
|
|
250
|
-
const { parseGpx, enhance, writeGpx } = engine;
|
|
251
|
-
|
|
252
|
-
const handle = parseGpx(gpxXml);
|
|
253
|
-
const out = await enhance(handle, { fixElevation: true }); // fetches DEM tiles, decodes WebP
|
|
254
|
-
const xml = writeGpx(out);
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
`enhance(..., { fixElevation: true })` auto-instantiates a default `ElevationProvider`
|
|
258
|
-
(mapterhorn Terrarium tiles) and runs the full pipeline (densify → fix elevation → smooth →
|
|
259
|
-
max speeds → virtualize → resample → simplify).
|
|
178
|
+
import * as engineRaw from '@glandais/vcyclist-engine';
|
|
179
|
+
const engine = engineRaw.io.github.glandais.engine;
|
|
260
180
|
|
|
261
|
-
|
|
181
|
+
const { parseGpx, enhance, writeGpx, pathSize, pathTotalDistance } = engine;
|
|
262
182
|
|
|
263
|
-
|
|
264
|
-
const
|
|
183
|
+
const path = parseGpx(gpxXml);
|
|
184
|
+
const out = await enhance(path, null); // physics only; { fixElevation: true } for DEM
|
|
185
|
+
console.log(pathSize(out), pathTotalDistance(out), 'm');
|
|
265
186
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
187
|
+
// `<power>` carries the SOURCE file's power by default — writing simulated data into a format the
|
|
188
|
+
// ecosystem reads as a recording is the caller's call. Ask for the simulation explicitly:
|
|
189
|
+
const xml = writeGpx(out, true, 'computed-or-input', 'my route');
|
|
269
190
|
```
|
|
270
191
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
#### Multi-track GPX
|
|
192
|
+
**[`docs/guides/using-from-javascript.md`](docs/guides/using-from-javascript.md)** covers the whole
|
|
193
|
+
façade — `enhanceWithCourse` and its five DTOs, FIT and CSV/JSON export, climbs, the racing-line
|
|
194
|
+
report, the standalone elevation API, and the Node/Bun specifics.
|
|
275
195
|
|
|
276
|
-
|
|
277
|
-
several `<trk>` or several `<trkseg>` :
|
|
196
|
+
### WASI — no JVM, no JavaScript
|
|
278
197
|
|
|
279
|
-
|
|
280
|
-
|
|
198
|
+
`:engine` links a standalone WASI module, so the whole pipeline runs inside wasmtime, WasmEdge,
|
|
199
|
+
wazero, or an embedding in Go, Rust, Python or the JVM.
|
|
281
200
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const xml = writeGpxTracks(tracks); // one <trk> per path
|
|
201
|
+
```bash
|
|
202
|
+
./gradlew :engine:wasmModule # -> engine/build/wasm/vcyclist-engine.wasm + .sha256
|
|
285
203
|
```
|
|
286
204
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
`parseGpxTracks` also returns `<rte>` routes, which many planners emit and which used to be
|
|
291
|
-
dropped silently. `parseGpxTracksOnly` and `parseGpxRoutesOnly` select one container or the other.
|
|
205
|
+
The host implements three imports — `read_input`, `write_output` and `fetch_tile` (which may simply
|
|
206
|
+
answer "no tile") — and everything else is numeric exports over integer handles:
|
|
292
207
|
|
|
293
|
-
|
|
208
|
+
```python
|
|
209
|
+
staged["bytes"] = open("ride.gpx", "rb").read()
|
|
210
|
+
handle = exports["vcParseGpx"](store, len(staged["bytes"]))
|
|
294
211
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
212
|
+
staged["bytes"] = b'{"computeOnePointPerSecond": true}'
|
|
213
|
+
out = exports["vcEnhance"](store, handle, len(staged["bytes"]))
|
|
214
|
+
print(exports["vcPathDurationMs"](store, out) / 1000, "s")
|
|
298
215
|
```
|
|
299
216
|
|
|
300
|
-
|
|
301
|
-
`
|
|
302
|
-
|
|
217
|
+
This is not a reduced surface: `vcEnhanceWithCourse`, `vcPathToFit`, `vcPathToCsv`,
|
|
218
|
+
`vcDetectClimbsJson` and `vcAnalyzeRacingLineJson` are all there, and §10 of the guide is a
|
|
219
|
+
function-by-function parity table against the JavaScript façade.
|
|
220
|
+
**[`docs/guides/wasm-wasi-abi.md`](docs/guides/wasm-wasi-abi.md)** is the full contract;
|
|
221
|
+
[`tools/wasi`](tools/wasi/README.md) is a working host that CI runs on every pull request.
|
|
303
222
|
|
|
304
|
-
|
|
305
|
-
(a pure-WASM WebP decoder, ~50 KB, listed as a runtime `dependency` of
|
|
306
|
-
`@glandais/vcyclist-engine` and `@glandais/vcyclist-elevation`). It is loaded lazily via
|
|
307
|
-
`eval('require')`, so browser bundlers do not pull it into the browser build. Requires
|
|
308
|
-
Node ≥ 18 (`globalThis.fetch` is built-in since Node 18 / Bun) ; Node 22+ recommended for
|
|
309
|
-
ESM `require()` support.
|
|
223
|
+
## Try the demo
|
|
310
224
|
|
|
311
|
-
|
|
225
|
+
**<https://glandais.github.io/vcyclist>** — no install, runs the real engine in your browser.
|
|
312
226
|
|
|
313
|
-
|
|
314
|
-
Kotlin/JS engine end-to-end in a browser (GPX upload, configurable cyclist /
|
|
315
|
-
bike / wind / power, chart + map, hover sync).
|
|
227
|
+
A Vue 3 + Leaflet + Chart.js app with two routes, both on the same Kotlin/JS bundle:
|
|
316
228
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
See [`demo/README.md`](demo/README.md) for the dev workflow and architecture.
|
|
323
|
-
|
|
324
|
-
## Build & test
|
|
229
|
+
- `#/` — **GPX analysis**: upload a route, run the physics pipeline, inspect every field on a
|
|
230
|
+
synchronized chart and map, with climb detection and the racing line, then download the result
|
|
231
|
+
as GPX or as a Garmin FIT course.
|
|
232
|
+
- `#/elevation` — **elevation explorer**: query DEM tiles at a point or along a path, with
|
|
233
|
+
smoothing, Douglas-Peucker simplification and hillshade/slope relief.
|
|
325
234
|
|
|
326
235
|
```bash
|
|
327
|
-
|
|
328
|
-
./gradlew :engine:allTests # engine tests across JVM / JS Node / JS browser
|
|
329
|
-
./gradlew :elevation:allTests # elevation tests
|
|
330
|
-
./gradlew :elevation:jvmTest --tests '*Integration*' \
|
|
331
|
-
-PINTEGRATION=1 # live HTTP tests against tiles.mapterhorn.com
|
|
332
|
-
./gradlew ktlintCheck # lint
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
## Layout
|
|
336
|
-
|
|
236
|
+
cd demo && npm run dev # http://localhost:3000, against a locally built engine
|
|
337
237
|
```
|
|
338
|
-
vcyclist/
|
|
339
|
-
├── settings.gradle.kts # multi-module Gradle KMP project
|
|
340
|
-
├── gradle/libs.versions.toml # version catalog (Kotlin 2.3.21, coroutines 1.11, xmlutil 0.91, …)
|
|
341
|
-
├── docs/
|
|
342
|
-
│ ├── PLAN.md # task-by-task progress (Phases 1-2bis)
|
|
343
|
-
│ ├── elevation-integration.md # how to run live HTTP integration tests
|
|
344
|
-
│ ├── kotlin-js-jvm-webp.md # Kotlin/JS ↔ JS interop guide
|
|
345
|
-
│ └── tasks/ # one Markdown per implementation task (00-31, + bonus demos)
|
|
346
|
-
├── elevation/ # :elevation KMP module
|
|
347
|
-
├── engine/ # :engine KMP module (depends on :elevation)
|
|
348
|
-
└── codegen/ # :codegen JVM helper for Path accessor generation
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
## Status
|
|
352
238
|
|
|
353
|
-
|
|
354
|
-
Douglas-Peucker 3D, smoother, LRU cache + TileManager, `ElevationProvider`, live HTTP integration.
|
|
355
|
-
- ✅ **Phase 2** — `:engine` module port (tasks 10-28) : Path model, Cyclist/Bike/Course,
|
|
356
|
-
GPX I/O, full physics, simulation, post-processing, `Enhancer`, CLI, `@JsExport` façades.
|
|
357
|
-
- ✅ **Phase 2bis** — pipeline fidelity fixes (tasks 29-31) : `VirtualizeService` last-point
|
|
358
|
-
timestamp, `PointPerDistance` port, integration into `Enhancer`.
|
|
359
|
-
- ✅ **Phase 3** — Node.js / Bun support (tasks 32-33) : runtime-detection in
|
|
360
|
-
`TileFetcher.js.kt` (browser path unchanged, Node path uses `globalThis.fetch` +
|
|
361
|
-
`@jsquash/webp` WASM decoder loaded via lazy `eval('require')`), webpack externals to keep
|
|
362
|
-
the browser bundle free of `@jsquash/webp`, `ElevationProvider` auto-instantiation in
|
|
363
|
-
`EngineJsApi.enhance` when `opts.fixElevation` is true (JS façade), 6 jsTest classes
|
|
364
|
-
gated by `INTEGRATION=1`.
|
|
365
|
-
|
|
366
|
-
Total `:engine` test coverage : 32 test classes / ~326 commonTest cases / 3 targets =
|
|
367
|
-
~1000 green executions, plus JVM-only smoke tests for the CLI and the full pipeline.
|
|
368
|
-
|
|
369
|
-
End-to-end smoke (after Phase 2bis) : sample.gpx (3569 source points, 130 km, ~4550 m gain)
|
|
370
|
-
runs through the complete `Enhancer` pipeline in ~1.7 s on JVM, producing ~1000 simplified
|
|
371
|
-
output points covering ~128.6 km / ~5.3 h of simulated ride.
|
|
239
|
+
See [`demo/README.md`](demo/README.md) for the architecture and the static-site build.
|
|
372
240
|
|
|
373
241
|
## Documentation
|
|
374
242
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
243
|
+
[`docs/README.md`](docs/README.md) is the index and says which documents are current and which are
|
|
244
|
+
frozen history. The short version:
|
|
245
|
+
|
|
246
|
+
- [`docs/guides/`](docs/README.md#guides) — how to use and extend the project: Java, JavaScript, the
|
|
247
|
+
WASI ABI, the racing line, the release flow
|
|
248
|
+
- [`docs/ledgers/`](docs/README.md#ledgers) — living state: research improvements, build warnings,
|
|
249
|
+
surface coverage
|
|
250
|
+
- [`docs/research/`](docs/research/README.md) — the solo-rider simulation research report
|
|
251
|
+
- Module documentation lives next to its module: [`cli/`](cli/README.md),
|
|
252
|
+
[`elevation/`](elevation/README.md), [`map/`](map/README.md), [`demo/`](demo/README.md)
|
|
382
253
|
|
|
383
254
|
## Contributing
|
|
384
255
|
|
|
385
|
-
`develop`
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
Every push to `develop` runs the full multi-target test suite via
|
|
389
|
-
`.github/workflows/release.yml` and, if green, lets semantic-release tag a new version,
|
|
390
|
-
publish to Maven Central + npm, and commit the version bump back to `develop` with
|
|
391
|
-
`[skip ci]`. See [`docs/publishing.md`](docs/publishing.md) for the full flow.
|
|
256
|
+
Open PRs against `develop` — the default and only long-lived branch — using
|
|
257
|
+
[Conventional Commits](https://www.conventionalcommits.org/). Build commands, module layout,
|
|
258
|
+
testing conventions and the release flow are in [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
392
259
|
|
|
393
260
|
## License
|
|
394
261
|
|
|
395
|
-
Apache License 2.0
|
|
396
|
-
metadata in `engine/build.gradle.kts` and `elevation/build.gradle.kts`. A top-level `LICENSE`
|
|
397
|
-
file will be added before the first public release.
|
|
262
|
+
[Apache License 2.0](LICENSE).
|
package/index.cjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// GENERATED by `./gradlew :codegen:generateTsFacade` — do not edit.
|
|
2
|
+
//
|
|
3
|
+
// The CommonJS half of the same unwrapping. Both spellings ship because the bundle is UMD
|
|
4
|
+
// and Node resolves a bare `.js` as CommonJS — an ESM-only wrapper breaks `require()`.
|
|
5
|
+
const root = require('./vcyclist-engine.js');
|
|
6
|
+
|
|
7
|
+
const ns = root.io?.github?.glandais?.engine ?? root;
|
|
8
|
+
|
|
9
|
+
const climbDefaults = Object.freeze({
|
|
10
|
+
minMinClimbElevationM: 10,
|
|
11
|
+
maxMinClimbElevationM: 35,
|
|
12
|
+
minClimbElevationRatio: 100,
|
|
13
|
+
minGradePercent: 3,
|
|
14
|
+
maxDiffRealGrade: 1.3,
|
|
15
|
+
booster: 1.3,
|
|
16
|
+
maxAnalysisPoints: 3000,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const csvDefaults = Object.freeze({
|
|
20
|
+
separator: ',',
|
|
21
|
+
unitsInHeader: true,
|
|
22
|
+
decimals: null,
|
|
23
|
+
lineSeparator: '\n',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const jsonDefaults = Object.freeze({
|
|
27
|
+
pretty: false,
|
|
28
|
+
decimals: null,
|
|
29
|
+
includeMeta: true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
parseGpxWaypoints: ns.parseGpxWaypoints,
|
|
34
|
+
parseGpx: ns.parseGpx,
|
|
35
|
+
parseGpxTracks: ns.parseGpxTracks,
|
|
36
|
+
parseGpxSegments: ns.parseGpxSegments,
|
|
37
|
+
parseGpxTracksOnly: ns.parseGpxTracksOnly,
|
|
38
|
+
parseGpxRoutesOnly: ns.parseGpxRoutesOnly,
|
|
39
|
+
writeGpxTracks: ns.writeGpxTracks,
|
|
40
|
+
pathSize: ns.pathSize,
|
|
41
|
+
pathTotalDistance: ns.pathTotalDistance,
|
|
42
|
+
pathDurationMs: ns.pathDurationMs,
|
|
43
|
+
pathElevationGain: ns.pathElevationGain,
|
|
44
|
+
pathElevationLoss: ns.pathElevationLoss,
|
|
45
|
+
pathElevationGainFiltered: ns.pathElevationGainFiltered,
|
|
46
|
+
pathElevationLossFiltered: ns.pathElevationLossFiltered,
|
|
47
|
+
pathReportedElevationGain: ns.pathReportedElevationGain,
|
|
48
|
+
pathReportedElevationLoss: ns.pathReportedElevationLoss,
|
|
49
|
+
pointAt: ns.pointAt,
|
|
50
|
+
writeGpx: ns.writeGpx,
|
|
51
|
+
writeGpxAt: ns.writeGpxAt,
|
|
52
|
+
enhance: ns.enhance,
|
|
53
|
+
enhanceWithCourse: ns.enhanceWithCourse,
|
|
54
|
+
getField: ns.getField,
|
|
55
|
+
fieldDefinitions: ns.fieldDefinitions,
|
|
56
|
+
pathLatitudeDeg: ns.pathLatitudeDeg,
|
|
57
|
+
pathLongitudeDeg: ns.pathLongitudeDeg,
|
|
58
|
+
pathToCsv: ns.pathToCsv,
|
|
59
|
+
pathToJson: ns.pathToJson,
|
|
60
|
+
pathToFit: ns.pathToFit,
|
|
61
|
+
pathsToFit: ns.pathsToFit,
|
|
62
|
+
dominantHeadwindAzimuth: ns.dominantHeadwindAzimuth,
|
|
63
|
+
dominantHeadwindAzimuthOfTracks: ns.dominantHeadwindAzimuthOfTracks,
|
|
64
|
+
detectClimbs: ns.detectClimbs,
|
|
65
|
+
detectClimbsWithOptions: ns.detectClimbsWithOptions,
|
|
66
|
+
analyzeRacingLine: ns.analyzeRacingLine,
|
|
67
|
+
climbDefaults,
|
|
68
|
+
csvDefaults,
|
|
69
|
+
jsonDefaults,
|
|
70
|
+
};
|