vcyclist
1.2.0indexedPhysics-based cycling simulator converting static GPX traces into virtual rides with realistic speeds, times and power estimates; elevation correction via Terrarium DEM, smoothing, resampling and GPX output.
Physics-based cycling simulator converting static GPX traces into virtual rides with realistic speeds, times and power estimates; elevation correction via Terrarium DEM, smoothing, resampling and GPX output.
Kotlin Multiplatform port of @glandais/virtual-cyclist:
physics-based cycling simulator that turns a static GPS trace into a virtualized ride with
realistic speeds, times and power estimates. Inspired by gpx2web
(Java) for the physics model and the @glandais/elevation
TypeScript library for elevation data.
┌──────────────┐
sample.gpx ────▶│ GpxParser │
└──────┬───────┘
▼
┌─────────────────────────────────────────┐
│ Enhancer (orchestrator) │
│ ├─ PointPerDistance(-1, 30) │
│ ├─ fixElevation (Terrarium tiles)* │
│ ├─ PointPerDistance(1, 2) │
│ ├─ smoothElevation ( m kernel) │
│ ├─ MaxSpeedComputer (cornering+braking)│
│ ├─ VirtualizeService ( Hz physics) │
│ ├─ PointPerSecond (uniform sampling) │
│ └─ PathSimplifier (Douglas-Peucker D) │
└──────────────────┬──────────────────────┘
▼
┌──────────────┐
│ GpxWriter │────▶ output
└──────────────┘
(*) optional — needs an ElevationProvider
npm install @glandais/vcyclist-engine # Kotlin/JS bundle
npm install @glandais/vcyclist-engine-wasm # Kotlin/Wasm bundle
npm install @glandais/vcyclist-elevation # Kotlin/JS bundle
npm install @glandais/vcyclist-elevation-wasm # Kotlin/Wasm bundle
// Gradle Kotlin DSL
dependencies {
implementation("io.github.glandais:vcyclist-engine:1.0.0") // pulls -jvm / -js / -wasm-js per target
implementation("io.github.glandais:vcyclist-elevation:1.0.0")
}
Replace 1.0.0 by the latest version shown in the badges above. KMP consumers automatically
get the platform-specific variant (-jvm, -js, -wasm-js) for their target.
See docs/publishing.md for the release process.
# Enhance a GPX file with the default cyclist (80 kg / 280 W) and bike (Crr 0.004) :
./gradlew :engine:run -Pargs="enhance path/to/input.gpx -o /tmp/output.gpx"
The CLI runs the full enhancement pipeline (no elevation correction — no HTTP) and writes the
simulated trace back to a GPX file. See engine/src/jvmMain/.../EngineCli.kt.
# Kotlin/Wasm demo
./gradlew :elevation:wasmJsBrowserDevelopmentRun
# Kotlin/JS demo (sibling, same UI)
./gradlew :elevation:jsBrowserDevelopmentRun
Both demos share the original TS demo UI (Leaflet +
Chart.js + GPX upload). See elevation/README.md for details.
import io.github.glandais.engine.Enhancer
import io.github.glandais.engine.gpx.GpxParser
import io.github.glandais.engine.gpx.firstTrackAsPath
suspend fun virtualize(xml: String): String {
val path = GpxParser.parse(xml).firstTrackAsPath()
val out = Enhancer.enhanceCourseDefault(path) // pure physics, no HTTP
return io.github.glandais.engine.gpx.GpxWriter.write(
.toGpxDocument(trackName = )
)
}
generateTypeScriptDefinitions() is enabled on both js(IR) and wasmJs, so you get a
.d.ts next to the bundle in build/dist/{js,wasmJs}/productionExecutable/vcyclist-engine.d.{ts,mts}.
The Kotlin/JS variant (@glandais/vcyclist-engine, @glandais/vcyclist-elevation) runs
in both browser and Node.js / Bun. The Wasm variants (*-wasm) are browser-only.
import { parseGpx, enhance, writeGpx, pathSize, pathTotalDistance } from '@glandais/vcyclist-engine';
const handle = parseGpx(gpxXml);
.(, (handle));
out = (handle, );
.(, (out), (out), );
xml = (out);
import { parseGpx, enhance, writeGpx } from '@glandais/vcyclist-engine';
const handle = parseGpx(gpxXml);
const out = await enhance(handle, { fixElevation: true }); // fetches DEM tiles, decodes WebP
const xml = writeGpx(out);
enhance(..., { fixElevation: true }) auto-instantiates a default ElevationProvider
(mapterhorn Terrarium tiles) and runs the full pipeline (densify → fix elevation → smooth →
max speeds → virtualize → resample → simplify).
On Node.js / Bun, tile decoding uses @jsquash/webp
(a pure-WASM WebP decoder, ~50 KB, listed as a runtime dependency of
@glandais/vcyclist-engine and @glandais/vcyclist-elevation). It is loaded lazily via
eval('require'), so browser bundlers do not pull it into the browser build. Requires
Node ≥ 18 ( is built-in since Node 18 / Bun) ; Node 22+ recommended for
ESM support.
The demo/ module is a Vue 3 + Vite frontend that exercises the
Kotlin/JS engine end-to-end in a browser (GPX upload, configurable cyclist /
bike / wind / power, chart + map, hover sync).
./gradlew :demo:assemble
python -m http.server -d demo/dist 8000 # or any static file server
See demo/README.md for the dev workflow and architecture.
./gradlew check # full build + all tests on all targets
./gradlew :engine:allTests # engine tests across JVM / JS Node / JS browser / Wasm browser
./gradlew :elevation:allTests # elevation tests
./gradlew :elevation:jvmTest --tests '*Integration*' \
-PINTEGRATION=1 # live HTTP tests against tiles.mapterhorn.com
./gradlew ktlintCheck # lint
vcyclist/
├── settings.gradle.kts # multi-module Gradle KMP project
├── gradle/libs.versions.toml # version catalog (Kotlin 2.3.21, coroutines 1.11, xmlutil 0.91, …)
├── docs/
│ ├── PLAN.md # task-by-task progress (Phases 1-2bis)
│ ├── parity.md # parity strategy vs the TS reference
│ ├── elevation-integration.md # how to run live HTTP integration tests
│ ├── kotlin-wasm-jvm-webp.md # Kotlin/Wasm ↔ JS interop guide
│ └── tasks/ # one Markdown per implementation task (00-31, + bonus demos)
├── elevation/ # :elevation KMP module
├── engine/ # :engine KMP module (depends on :elevation)
└── codegen/
Total :engine test coverage : 32 test classes / ~326 commonTest cases / 4 targets =
~1300 green executions, plus JVM-only smoke tests for the CLI and the full pipeline.
End-to-end smoke (after Phase 2bis) : sample.gpx (3569 source points, 130 km, ~4550 m gain)
runs through the complete Enhancer pipeline in ~1.7 s on JVM, producing ~1000 simplified
output points covering ~128.6 km / ~5.3 h of simulated ride.
is the — there is no . Open PRs against using : triggers a minor release, a patch, anything else is a no-op release-wise. Every push to runs the full multi-target test suite via and, if green, lets semantic-release tag a new version, publish to Maven Central + npm, and commit the version bump back to with . See for the full flow.
Apache License 2.0, aligned with the upstream gpx2web project. See the Maven Central POM
metadata in engine/build.gradle.kts and elevation/build.gradle.kts. A top-level LICENSE
file will be added before the first public release.
| Module | Purpose | Targets |
|---|
:elevation | Terrarium tile fetch + DEM lookup + Haversine + Douglas-Peucker 3D + triangular smoother. See elevation/README.md. | JVM, JS Node, JS browser, Wasm browser |
:engine | Path model (36 fields × DoubleArray), physics (4 resistive PowerProviders + cyclist input + MaxSpeedComputer + VirtualizeService), GPX I/O, Enhancer pipeline, JVM CLI. | JVM, JS Node, JS browser, Wasm browser |
:codegen | Tiny build-time helper that regenerates GeneratedPath.kt + PointFieldAccessors.kt from PointField (run only when the field list changes). | JVM only |
globalThis.fetchrequire():elevation module port (tasks 00-09) : Terrarium tiles, Haversine, ECEF,
Douglas-Peucker 3D, smoother, LRU cache + TileManager, ElevationProvider, live HTTP integration.:engine module port (tasks 10-28) : Path model, Cyclist/Bike/Course,
GPX I/O, full physics, simulation, post-processing, Enhancer, CLI, @JsExport façades.VirtualizeService last-point
timestamp, PointPerDistance port, integration into Enhancer.TileFetcher.js.kt (browser path unchanged, Node path uses globalThis.fetch +
@jsquash/webp WASM decoder loaded via lazy eval('require')), webpack externals to keep
the browser bundle free of @jsquash/webp, ElevationProvider auto-instantiation in
EngineJsApi.enhance when opts.fixElevation is true (JS + Wasm façades), 6 jsTest classes
gated by INTEGRATION=1.docs/PLAN.md — task-by-task plan with commit hashes for every step.docs/tasks/ — detailed Markdown spec for each task (00-31 + bonus demos).docs/parity.md — TS↔Kotlin parity approach and tolerances.docs/kotlin-wasm-jvm-webp.md — Kotlin/Wasm ↔ JS interop
guide that underpins the @JsExport façades and the WebP tile decoding.docs/publishing.md — release flow (Maven Central + npm via
semantic-release on push to develop).elevation/README.md — :elevation module details + browser demos.developmaindevelopfeat:fix:develop.github/workflows/release.ymldevelop[skip ci]Surfaced from shared tags and platforms — no rankings paid for.