@glandais/vcyclist-engine 4.2.1 → 4.3.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 +139 -275
- package/kotlin-kotlin-stdlib.js +7 -7
- package/kotlinx-coroutines-core.js +2 -2
- package/package.json +1 -1
- package/vcyclist-elevation.js.map +1 -1
- package/vcyclist-engine.d.ts +3 -3
- package/vcyclist-engine.js +77 -55
- package/vcyclist-engine.js.map +1 -1
- package/vcyclist-fit.js.map +1 -1
- package/vcyclist-gpx.js +150 -78
- 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
|
┌──────────────┐
|
|
@@ -40,358 +39,223 @@ TypeScript library for elevation data.
|
|
|
40
39
|
(*) optional — needs an ElevationProvider
|
|
41
40
|
```
|
|
42
41
|
|
|
43
|
-
##
|
|
42
|
+
## What it can do
|
|
44
43
|
|
|
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 |
|
|
44
|
+
**In**: GPX tracks, routes (`<rte>`), segments and waypoints.
|
|
54
45
|
|
|
55
|
-
|
|
46
|
+
**Physics**: elevation correction from DEM tiles, curvature estimation or an optimal racing line,
|
|
47
|
+
cornering and braking sharing one friction-ellipse budget, a configurable rider and bike, and a
|
|
48
|
+
1 Hz time-stepping simulation that produces speed, time and power at every point.
|
|
56
49
|
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
**Out**: GPX, Garmin FIT courses, CSV, column-oriented JSON, static PNG maps — plus climb
|
|
51
|
+
detection, a racing-line report and worst-case wind analysis.
|
|
59
52
|
|
|
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.
|
|
53
|
+
Four doors reach the same engine, and a capability is available from all of them unless noted:
|
|
73
54
|
|
|
74
|
-
|
|
55
|
+
| Capability | CLI | Kotlin / Java | JavaScript / TS | WASI |
|
|
56
|
+
|---|---|---|---|---|
|
|
57
|
+
| Run the pipeline | `enhance` | `Enhancer.enhanceCourseDefault` | `enhance` | `vcEnhance` |
|
|
58
|
+
| Configure rider, bike, wind, power | `--cyclist-*`, `--bike-*`, `--wind-*` | `CoursePhysics(Course(…))` | `enhanceWithCourse` | `vcEnhanceWithCourse` |
|
|
59
|
+
| Power models `constant` · `durability` · `critical-power` · `from_data` | `--cyclist-model` | `CyclistPowerSpec` | `power.type` | `power.type` |
|
|
60
|
+
| Terrain pacing, power slew limit | `--cyclist-pacing`, `--cyclist-slew` | provider decorators | `power.pacing`, `power.maxSlewWPerS` | idem |
|
|
61
|
+
| Road condition, dry or wet | `--road-condition` | `Cyclist.withRoadCondition` | `cyclist.roadCondition` | idem |
|
|
62
|
+
| Pedal-strike clearance | `--bike-max-pedal-angle` | `Bike.maxPedalingLeanAngleDeg` | `bike.maxPedalingLeanAngleDeg` | idem |
|
|
63
|
+
| Racing line + corridor mode | `--racing-line`, `--corridor` | `RacingLineOptions` | `racingLineEnabled` | idem |
|
|
64
|
+
| DEM elevation correction | `--fix-elevation` | `ElevationProvider` | `fixElevation: true` | host serves tiles |
|
|
65
|
+
| Climb detection | — | `ClimbDetector.detect` | `detectClimbs` | `vcDetectClimbsJson` |
|
|
66
|
+
| Racing-line report | `--racing-line-report` | `RacingLine.analyze` | `analyzeRacingLine` | `vcAnalyzeRacingLineJson` |
|
|
67
|
+
| Write GPX / CSV / JSON | `--gpx --csv --json` | `GpxWriter`, `CsvWriter`, `JsonWriter` | `writeGpx`, `pathToCsv`, `pathToJson` | `vcWriteGpx`, `vcPathToCsv/Json` |
|
|
68
|
+
| Write FIT course | `--fit` | `Path.toFitBytes` | `pathToFit`, `pathsToFit` | `vcPathToFit`, `vcPathsToFit` |
|
|
69
|
+
| Static map PNG | `export --map` | `MapFactoriesJvm` | — | — |
|
|
75
70
|
|
|
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).
|
|
71
|
+
Static maps are JVM-only by construction — `:map` draws on `java.awt`.
|
|
72
|
+
[`docs/ledgers/surface-coverage.md`](docs/ledgers/surface-coverage.md) tracks this matrix as
|
|
73
|
+
capabilities land, so that a feature cannot reach one door and quietly miss the others.
|
|
81
74
|
|
|
82
75
|
## Install
|
|
83
76
|
|
|
84
|
-
### npm
|
|
77
|
+
### npm — browser, Node.js and Bun
|
|
85
78
|
|
|
86
79
|
```bash
|
|
87
|
-
npm install @glandais/vcyclist-engine #
|
|
88
|
-
npm install @glandais/vcyclist-elevation #
|
|
80
|
+
npm install @glandais/vcyclist-engine # physics, GPX, FIT, CSV/JSON, climbs, racing line
|
|
81
|
+
npm install @glandais/vcyclist-elevation # DEM lookups on their own
|
|
89
82
|
```
|
|
90
83
|
|
|
91
|
-
|
|
84
|
+
The engine bundle already carries the elevation façade; install the second package only if you
|
|
85
|
+
want DEM lookups without the physics.
|
|
86
|
+
|
|
87
|
+
### Gradle — JVM or Kotlin Multiplatform
|
|
92
88
|
|
|
93
89
|
```kotlin
|
|
94
|
-
// Gradle Kotlin DSL
|
|
95
90
|
dependencies {
|
|
96
|
-
implementation("io.github.glandais:vcyclist-engine:
|
|
97
|
-
implementation("io.github.glandais:vcyclist-elevation:
|
|
91
|
+
implementation("io.github.glandais:vcyclist-engine:4.2.1") // pulls -jvm / -js per target
|
|
92
|
+
implementation("io.github.glandais:vcyclist-elevation:4.2.1")
|
|
98
93
|
}
|
|
99
94
|
```
|
|
100
95
|
|
|
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.
|
|
96
|
+
### Maven
|
|
105
97
|
|
|
106
|
-
|
|
98
|
+
```xml
|
|
99
|
+
<dependency>
|
|
100
|
+
<groupId>io.github.glandais</groupId>
|
|
101
|
+
<artifactId>vcyclist-engine-jvm</artifactId>
|
|
102
|
+
<version>4.2.1</version>
|
|
103
|
+
</dependency>
|
|
104
|
+
```
|
|
107
105
|
|
|
108
|
-
|
|
106
|
+
The badges above are the source of truth for the current version. KMP consumers get the
|
|
107
|
+
platform-specific variant automatically; from plain Maven, name the `-jvm` artifact yourself.
|
|
109
108
|
|
|
110
|
-
|
|
109
|
+
`vcyclist-gpx` (the `Path` model + GPX I/O) comes in transitively via `vcyclist-engine`, and can be
|
|
110
|
+
depended on alone if you only need parsing and resampling. `vcyclist-fit` and `vcyclist-map` are
|
|
111
|
+
published separately.
|
|
111
112
|
|
|
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:
|
|
113
|
+
### CLI and `.wasm`
|
|
115
114
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
java -jar cli/build/libs/vcyclist-cli-*-all.jar enhance route.gpx --gpx out.gpx --csv out.csv
|
|
115
|
+
The CLI is an application, not a library, so it is **not** on Maven Central: download
|
|
116
|
+
`vcyclist-cli-<version>-all.jar` from a [GitHub release](https://github.com/glandais/vcyclist/releases).
|
|
117
|
+
The WASI module is built from source (below) rather than published to a registry.
|
|
120
118
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
**Requirements**: Java 21+ for the JVM and CLI; Node ≥ 18 (22+ recommended) or Bun for JavaScript;
|
|
120
|
+
a WASI runtime with the `function-references`, `gc` and `exceptions` proposals — wasmtime 46+ is
|
|
121
|
+
known good.
|
|
124
122
|
|
|
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).
|
|
123
|
+
## Quick start
|
|
129
124
|
|
|
130
|
-
###
|
|
125
|
+
### Command line
|
|
131
126
|
|
|
132
127
|
```bash
|
|
133
|
-
|
|
134
|
-
./gradlew :elevation:jsBrowserDevelopmentRun
|
|
128
|
+
java -jar vcyclist-cli-*-all.jar enhance route.gpx --gpx out.gpx --csv out.csv
|
|
135
129
|
```
|
|
136
130
|
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
`enhance` runs the physics pipeline; `export` produces maps, FIT, CSV and JSON from a file you
|
|
132
|
+
already have. Elevation correction is off unless you pass `--fix-elevation`, so nothing touches the
|
|
133
|
+
network by default.
|
|
134
|
+
|
|
135
|
+
Full option reference, the rider models and what each is measured to be worth, and exit codes:
|
|
136
|
+
[`cli/README.md`](cli/README.md).
|
|
139
137
|
|
|
140
|
-
###
|
|
138
|
+
### Kotlin
|
|
141
139
|
|
|
142
140
|
```kotlin
|
|
143
141
|
import io.github.glandais.engine.Enhancer
|
|
144
142
|
import io.github.glandais.engine.gpx.GpxParser
|
|
143
|
+
import io.github.glandais.engine.gpx.GpxWriter
|
|
145
144
|
import io.github.glandais.engine.gpx.firstTrackAsPath
|
|
145
|
+
import io.github.glandais.engine.gpx.toGpxDocument
|
|
146
146
|
|
|
147
147
|
suspend fun virtualize(xml: String): String {
|
|
148
148
|
val path = GpxParser.parse(xml).firstTrackAsPath()
|
|
149
|
-
val out = Enhancer.enhanceCourseDefault(path)
|
|
150
|
-
return
|
|
151
|
-
out.toGpxDocument(trackName = "virtualized")
|
|
152
|
-
)
|
|
149
|
+
val out = Enhancer.enhanceCourseDefault(path) // pure physics, no HTTP
|
|
150
|
+
return GpxWriter.write(out.toGpxDocument(trackName = "virtualized"))
|
|
153
151
|
}
|
|
154
152
|
```
|
|
155
153
|
|
|
156
|
-
|
|
154
|
+
Pass an `ElevationProvider` as the second argument to correct elevations from DEM tiles, and an
|
|
155
|
+
`EnhanceOptions` as the third to configure the pipeline. For a configured rider, build a
|
|
156
|
+
`CoursePhysics(Course(path, cyclist, bike), …)` and call `Enhancer.enhanceCourse`.
|
|
157
157
|
|
|
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 |
|
|
158
|
+
### Java
|
|
166
159
|
|
|
167
160
|
```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:
|
|
161
|
+
Path input = GpxToPathJvm.firstTrackAsPath(GpxParserJvm.parse(xml));
|
|
180
162
|
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
163
|
String out = GpxWriterJvm.write(enhanced);
|
|
187
164
|
```
|
|
188
165
|
|
|
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
|
-
```
|
|
166
|
+
Every entry point has a `…Jvm` twin that restores Kotlin's default arguments, and every `suspend`
|
|
167
|
+
function has both a `…Blocking` and a `…Async` (`CompletableFuture`) bridge.
|
|
168
|
+
**[`docs/guides/using-from-java.md`](docs/guides/using-from-java.md)** has the calling rules — some
|
|
169
|
+
of them matter, `…Blocking` on a UI thread being the obvious one.
|
|
231
170
|
|
|
232
|
-
|
|
233
|
-
adds TypeScript types for the DTOs, which Kotlin/JS emits as referenced names only.
|
|
171
|
+
### JavaScript / TypeScript
|
|
234
172
|
|
|
235
|
-
|
|
173
|
+
Kotlin/JS emits a UMD bundle that preserves the package namespace, so there is exactly one
|
|
174
|
+
top-level export. Named imports do not work — unwrap it once:
|
|
236
175
|
|
|
237
176
|
```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).
|
|
177
|
+
import * as engineRaw from '@glandais/vcyclist-engine';
|
|
178
|
+
const engine = engineRaw.io.github.glandais.engine;
|
|
260
179
|
|
|
261
|
-
|
|
180
|
+
const { parseGpx, enhance, writeGpx, pathSize, pathTotalDistance } = engine;
|
|
262
181
|
|
|
263
|
-
|
|
264
|
-
const
|
|
182
|
+
const path = parseGpx(gpxXml);
|
|
183
|
+
const out = await enhance(path, null); // physics only; { fixElevation: true } for DEM
|
|
184
|
+
console.log(pathSize(out), pathTotalDistance(out), 'm');
|
|
265
185
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
186
|
+
// `<power>` carries the SOURCE file's power by default — writing simulated data into a format the
|
|
187
|
+
// ecosystem reads as a recording is the caller's call. Ask for the simulation explicitly:
|
|
188
|
+
const xml = writeGpx(out, true, 'computed-or-input', 'my route');
|
|
269
189
|
```
|
|
270
190
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
#### Multi-track GPX
|
|
191
|
+
**[`docs/guides/using-from-javascript.md`](docs/guides/using-from-javascript.md)** covers the whole
|
|
192
|
+
façade — `enhanceWithCourse` and its five DTOs, FIT and CSV/JSON export, climbs, the racing-line
|
|
193
|
+
report, the standalone elevation API, and the Node/Bun specifics.
|
|
275
194
|
|
|
276
|
-
|
|
277
|
-
several `<trk>` or several `<trkseg>` :
|
|
195
|
+
### WASI — no JVM, no JavaScript
|
|
278
196
|
|
|
279
|
-
|
|
280
|
-
|
|
197
|
+
`:engine` links a standalone WASI module, so the whole pipeline runs inside wasmtime, WasmEdge,
|
|
198
|
+
wazero, or an embedding in Go, Rust, Python or the JVM.
|
|
281
199
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const xml = writeGpxTracks(tracks); // one <trk> per path
|
|
200
|
+
```bash
|
|
201
|
+
./gradlew :engine:wasmModule # -> engine/build/wasm/vcyclist-engine.wasm + .sha256
|
|
285
202
|
```
|
|
286
203
|
|
|
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.
|
|
204
|
+
The host implements three imports — `read_input`, `write_output` and `fetch_tile` (which may simply
|
|
205
|
+
answer "no tile") — and everything else is numeric exports over integer handles:
|
|
292
206
|
|
|
293
|
-
|
|
207
|
+
```python
|
|
208
|
+
staged["bytes"] = open("ride.gpx", "rb").read()
|
|
209
|
+
handle = exports["vcParseGpx"](store, len(staged["bytes"]))
|
|
294
210
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
211
|
+
staged["bytes"] = b'{"computeOnePointPerSecond": true}'
|
|
212
|
+
out = exports["vcEnhance"](store, handle, len(staged["bytes"]))
|
|
213
|
+
print(exports["vcPathDurationMs"](store, out) / 1000, "s")
|
|
298
214
|
```
|
|
299
215
|
|
|
300
|
-
|
|
301
|
-
`
|
|
302
|
-
|
|
216
|
+
This is not a reduced surface: `vcEnhanceWithCourse`, `vcPathToFit`, `vcPathToCsv`,
|
|
217
|
+
`vcDetectClimbsJson` and `vcAnalyzeRacingLineJson` are all there, and §10 of the guide is a
|
|
218
|
+
function-by-function parity table against the JavaScript façade.
|
|
219
|
+
**[`docs/guides/wasm-wasi-abi.md`](docs/guides/wasm-wasi-abi.md)** is the full contract;
|
|
220
|
+
[`tools/wasi`](tools/wasi/README.md) is a working host that CI runs on every pull request.
|
|
303
221
|
|
|
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.
|
|
222
|
+
## Try the demo
|
|
310
223
|
|
|
311
|
-
|
|
224
|
+
**<https://glandais.github.io/vcyclist>** — no install, runs the real engine in your browser.
|
|
312
225
|
|
|
313
|
-
|
|
314
|
-
Kotlin/JS engine end-to-end in a browser (GPX upload, configurable cyclist /
|
|
315
|
-
bike / wind / power, chart + map, hover sync).
|
|
226
|
+
A Vue 3 + Leaflet + Chart.js app with two routes, both on the same Kotlin/JS bundle:
|
|
316
227
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
See [`demo/README.md`](demo/README.md) for the dev workflow and architecture.
|
|
323
|
-
|
|
324
|
-
## Build & test
|
|
228
|
+
- `#/` — **GPX analysis**: upload a route, run the physics pipeline, inspect every field on a
|
|
229
|
+
synchronized chart and map, with climb detection and the racing line, then download the result
|
|
230
|
+
as GPX or as a Garmin FIT course.
|
|
231
|
+
- `#/elevation` — **elevation explorer**: query DEM tiles at a point or along a path, with
|
|
232
|
+
smoothing, Douglas-Peucker simplification and hillshade/slope relief.
|
|
325
233
|
|
|
326
234
|
```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
|
-
|
|
235
|
+
cd demo && npm run dev # http://localhost:3000, against a locally built engine
|
|
337
236
|
```
|
|
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
237
|
|
|
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.
|
|
238
|
+
See [`demo/README.md`](demo/README.md) for the architecture and the static-site build.
|
|
372
239
|
|
|
373
240
|
## Documentation
|
|
374
241
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
242
|
+
[`docs/README.md`](docs/README.md) is the index and says which documents are current and which are
|
|
243
|
+
frozen history. The short version:
|
|
244
|
+
|
|
245
|
+
- [`docs/guides/`](docs/README.md#guides) — how to use and extend the project: Java, JavaScript, the
|
|
246
|
+
WASI ABI, the racing line, the release flow
|
|
247
|
+
- [`docs/ledgers/`](docs/README.md#ledgers) — living state: research improvements, build warnings,
|
|
248
|
+
surface coverage
|
|
249
|
+
- [`docs/research/`](docs/research/README.md) — the solo-rider simulation research report
|
|
250
|
+
- Module documentation lives next to its module: [`cli/`](cli/README.md),
|
|
251
|
+
[`elevation/`](elevation/README.md), [`map/`](map/README.md), [`demo/`](demo/README.md)
|
|
382
252
|
|
|
383
253
|
## Contributing
|
|
384
254
|
|
|
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.
|
|
255
|
+
Open PRs against `develop` — the default and only long-lived branch — using
|
|
256
|
+
[Conventional Commits](https://www.conventionalcommits.org/). Build commands, module layout,
|
|
257
|
+
testing conventions and the release flow are in [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
392
258
|
|
|
393
259
|
## License
|
|
394
260
|
|
|
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.
|
|
261
|
+
[Apache License 2.0](LICENSE).
|
package/kotlin-kotlin-stdlib.js
CHANGED
|
@@ -153,23 +153,23 @@ if (typeof String.prototype.startsWith === 'undefined') {
|
|
|
153
153
|
initMetadataForClass(AbstractMutableCollection, 'AbstractMutableCollection', VOID, AbstractCollection, [Collection, MutableIterable]);
|
|
154
154
|
initMetadataForClass(IteratorImpl, 'IteratorImpl');
|
|
155
155
|
initMetadataForClass(ListIteratorImpl, 'ListIteratorImpl', VOID, IteratorImpl);
|
|
156
|
-
initMetadataForClass(AbstractMutableList, 'AbstractMutableList', VOID, AbstractMutableCollection, [KtList,
|
|
156
|
+
initMetadataForClass(AbstractMutableList, 'AbstractMutableList', VOID, AbstractMutableCollection, [KtList, MutableIterable, Collection]);
|
|
157
157
|
initMetadataForClass(AbstractMap, 'AbstractMap', VOID, VOID, [KtMap]);
|
|
158
158
|
initMetadataForClass(AbstractMutableMap, 'AbstractMutableMap', VOID, AbstractMap, [KtMap]);
|
|
159
|
-
initMetadataForClass(AbstractMutableSet, 'AbstractMutableSet', VOID, AbstractMutableCollection, [
|
|
159
|
+
initMetadataForClass(AbstractMutableSet, 'AbstractMutableSet', VOID, AbstractMutableCollection, [KtSet, MutableIterable, Collection]);
|
|
160
160
|
initMetadataForCompanion(Companion_2);
|
|
161
161
|
initMetadataForInterface(RandomAccess, 'RandomAccess');
|
|
162
|
-
initMetadataForClass(ArrayList, 'ArrayList', ArrayList_init_$Create$, AbstractMutableList, [KtList,
|
|
162
|
+
initMetadataForClass(ArrayList, 'ArrayList', ArrayList_init_$Create$, AbstractMutableList, [KtList, MutableIterable, Collection, RandomAccess]);
|
|
163
163
|
initMetadataForClass(HashMap, 'HashMap', HashMap_init_$Create$, AbstractMutableMap, [KtMap]);
|
|
164
|
-
initMetadataForClass(HashMapKeys, 'HashMapKeys', VOID, AbstractMutableSet, [
|
|
164
|
+
initMetadataForClass(HashMapKeys, 'HashMapKeys', VOID, AbstractMutableSet, [KtSet, MutableIterable, Collection]);
|
|
165
165
|
initMetadataForClass(HashMapValues, 'HashMapValues', VOID, AbstractMutableCollection, [Collection, MutableIterable]);
|
|
166
|
-
initMetadataForClass(HashMapEntrySetBase, 'HashMapEntrySetBase', VOID, AbstractMutableSet, [
|
|
166
|
+
initMetadataForClass(HashMapEntrySetBase, 'HashMapEntrySetBase', VOID, AbstractMutableSet, [KtSet, MutableIterable, Collection]);
|
|
167
167
|
initMetadataForClass(HashMapEntrySet, 'HashMapEntrySet', VOID, HashMapEntrySetBase);
|
|
168
168
|
initMetadataForClass(HashMapKeysDefault$iterator$1);
|
|
169
169
|
initMetadataForClass(HashMapKeysDefault, 'HashMapKeysDefault', VOID, AbstractMutableSet);
|
|
170
170
|
initMetadataForClass(HashMapValuesDefault$iterator$1);
|
|
171
171
|
initMetadataForClass(HashMapValuesDefault, 'HashMapValuesDefault', VOID, AbstractMutableCollection);
|
|
172
|
-
initMetadataForClass(HashSet, 'HashSet', HashSet_init_$Create$, AbstractMutableSet, [
|
|
172
|
+
initMetadataForClass(HashSet, 'HashSet', HashSet_init_$Create$, AbstractMutableSet, [KtSet, MutableIterable, Collection]);
|
|
173
173
|
initMetadataForCompanion(Companion_3);
|
|
174
174
|
initMetadataForClass(Itr, 'Itr');
|
|
175
175
|
initMetadataForClass(KeysItr, 'KeysItr', VOID, Itr);
|
|
@@ -214,7 +214,7 @@ if (typeof String.prototype.startsWith === 'undefined') {
|
|
|
214
214
|
initMetadataForInterface(InternalMap, 'InternalMap');
|
|
215
215
|
initMetadataForClass(InternalHashMap, 'InternalHashMap', InternalHashMap_init_$Create$, VOID, [InternalMap]);
|
|
216
216
|
initMetadataForClass(LinkedHashMap, 'LinkedHashMap', LinkedHashMap_init_$Create$, HashMap, [KtMap]);
|
|
217
|
-
initMetadataForClass(LinkedHashSet, 'LinkedHashSet', LinkedHashSet_init_$Create$, HashSet, [
|
|
217
|
+
initMetadataForClass(LinkedHashSet, 'LinkedHashSet', LinkedHashSet_init_$Create$, HashSet, [KtSet, MutableIterable, Collection]);
|
|
218
218
|
initMetadataForInterface(Continuation, 'Continuation');
|
|
219
219
|
initMetadataForClass(InterceptedCoroutine, 'InterceptedCoroutine', VOID, VOID, [Continuation]);
|
|
220
220
|
initMetadataForClass(CoroutineImpl, 'CoroutineImpl', VOID, InterceptedCoroutine, [Continuation]);
|
|
@@ -107,8 +107,8 @@
|
|
|
107
107
|
var stackTraceToString = kotlin_kotlin.$_$.sa;
|
|
108
108
|
var removeFirstOrNull = kotlin_kotlin.$_$.x3;
|
|
109
109
|
var KtList = kotlin_kotlin.$_$.o2;
|
|
110
|
-
var Collection = kotlin_kotlin.$_$.n2;
|
|
111
110
|
var MutableIterable = kotlin_kotlin.$_$.q2;
|
|
111
|
+
var Collection = kotlin_kotlin.$_$.n2;
|
|
112
112
|
//endregion
|
|
113
113
|
//region block: pre-declaration
|
|
114
114
|
initMetadataForInterface(ParentJob, 'ParentJob', VOID, VOID, [Element], [0]);
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
initMetadataForClass(SemaphoreSegment, 'SemaphoreSegment', VOID, Segment);
|
|
185
185
|
initMetadataForClass(SetTimeoutBasedDispatcher, 'SetTimeoutBasedDispatcher', VOID, CoroutineDispatcher, VOID, [1]);
|
|
186
186
|
initMetadataForObject(NodeDispatcher, 'NodeDispatcher', VOID, SetTimeoutBasedDispatcher, VOID, [1]);
|
|
187
|
-
initMetadataForClass(MessageQueue, 'MessageQueue', VOID, VOID, [KtList,
|
|
187
|
+
initMetadataForClass(MessageQueue, 'MessageQueue', VOID, VOID, [KtList, MutableIterable, Collection]);
|
|
188
188
|
initMetadataForClass(ScheduledMessageQueue, 'ScheduledMessageQueue', VOID, MessageQueue);
|
|
189
189
|
initMetadataForClass(WindowMessageQueue, 'WindowMessageQueue', VOID, MessageQueue);
|
|
190
190
|
initMetadataForObject(Dispatchers, 'Dispatchers');
|