Skip to content
glandaisPublic

About

Kotlin Multiplatform physics-based cycling simulator: turns GPS traces into virtualized rides with realistic speed, time and power. JVM, JS/npm and WASI.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

347 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

vcyclist

npm engine npm elevation Maven Central Maven Central elevation Maven Central gpx Maven Central fit Maven Central map

Kotlin Multiplatform physics-based cycling simulator: it turns a static GPS trace into a virtualized ride with realistic speeds, times and power estimates. Elevation data comes from Terrarium-encoded DEM tiles β€” mapterhorn by default β€” fetched and decoded by the :elevation module.

                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        sample.gpx ────▢│ GpxParser    β”‚
                        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                               β–Ό
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚  Enhancer (orchestrator)                β”‚
            β”‚  β”œβ”€ PointPerDistance(-1, 30)            β”‚
            β”‚  β”œβ”€ fixElevation (Terrarium tiles)*     β”‚
            β”‚  β”œβ”€ PointPerDistance(1, 2)              β”‚
            β”‚  β”œβ”€ smoothElevation (150 m kernel)      β”‚
            β”‚  β”œβ”€ PathCurvature (turn radius)         β”‚
            β”‚  β”‚   or RacingLine (optimal line)*      β”‚
            β”‚  β”œβ”€ MaxSpeedComputer (cornering+braking)β”‚
            β”‚  β”œβ”€ VirtualizeService (1 Hz physics)    β”‚
            β”‚  β”œβ”€ PointPerSecond (uniform sampling)   β”‚
            β”‚  β”œβ”€ Wβ€²bal (Critical Power annotation)   β”‚
            β”‚  β”œβ”€ PathSimplifier (Douglas-Peucker 3D) β”‚
            β”‚  └─ ElevationGain (D+/Dβˆ’ dead band)     β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β–Ό
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚ GpxWriter    │────▢ output.gpx
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            (*) optional β€” needs an ElevationProvider

What it can do

In: GPX tracks, routes (<rte>), segments and waypoints.

Physics: elevation correction from DEM tiles, curvature estimation or an optimal racing line, cornering and braking sharing one friction-ellipse budget, a configurable rider and bike, and a 1 Hz time-stepping simulation that produces speed, time and power at every point.

Out: GPX, Garmin FIT courses, CSV, column-oriented JSON, static PNG maps β€” plus climb detection, a racing-line report and worst-case wind analysis.

Four doors reach the same engine, and a capability is available from all of them unless noted:

Capability CLI Kotlin / Java JavaScript / TS WASI
Run the pipeline enhance Enhancer.enhanceCourseDefault enhance vcEnhance
Configure rider, bike, wind, power --cyclist-*, --bike-*, --wind-* CoursePhysics(Course(…)) enhanceWithCourse vcEnhanceWithCourse
Power models constant Β· durability Β· critical-power Β· from_data --cyclist-model CyclistPowerSpec power.type power.type
Terrain pacing, power slew limit --cyclist-pacing, --cyclist-slew provider decorators power.pacing, power.maxSlewWPerS idem
Road condition, dry or wet --road-condition Cyclist.withRoadCondition cyclist.roadCondition idem
Pedal-strike clearance --bike-max-pedal-angle Bike.maxPedalingLeanAngleDeg bike.maxPedalingLeanAngleDeg idem
Racing line + corridor mode --racing-line, --corridor RacingLineOptions racingLineEnabled idem
DEM elevation correction --fix-elevation ElevationProvider fixElevation: true host serves tiles
Climb detection β€” ClimbDetector.detect detectClimbs vcDetectClimbsJson
Racing-line report --racing-line-report RacingLine.analyze analyzeRacingLine vcAnalyzeRacingLineJson
Write GPX / CSV / JSON --gpx --csv --json GpxWriter, CsvWriter, JsonWriter writeGpx, pathToCsv, pathToJson vcWriteGpx, vcPathToCsv/Json
Write FIT course --fit Path.toFitBytes pathToFit, pathsToFit vcPathToFit, vcPathsToFit
Static map PNG export --map MapFactoriesJvm β€” β€”

Static maps are JVM-only by construction β€” :map draws on java.awt. docs/ledgers/surface-coverage.md tracks this matrix as capabilities land, so that a feature cannot reach one door and quietly miss the others.

Install

npm β€” browser, Node.js and Bun

npm install @glandais/vcyclist-engine          # physics, GPX, FIT, CSV/JSON, climbs, racing line
npm install @glandais/vcyclist-elevation       # DEM lookups on their own

The engine bundle already carries the elevation faΓ§ade; install the second package only if you want DEM lookups without the physics.

Gradle β€” JVM or Kotlin Multiplatform

dependencies {
    implementation("io.github.glandais:vcyclist-engine:4.2.1")     // pulls -jvm / -js per target
    implementation("io.github.glandais:vcyclist-elevation:4.2.1")
}

Maven

<dependency>
  <groupId>io.github.glandais</groupId>
  <artifactId>vcyclist-engine-jvm</artifactId>
  <version>4.2.1</version>
</dependency>

The badges above are the source of truth for the current version. KMP consumers get the platform-specific variant automatically; from plain Maven, name the -jvm artifact yourself.

vcyclist-gpx (the Path model + GPX I/O) comes in transitively via vcyclist-engine, and can be depended on alone if you only need parsing and resampling. vcyclist-fit and vcyclist-map are published separately.

CLI and .wasm

The CLI is an application, not a library, so it is not on Maven Central: download vcyclist-cli-<version>-all.jar from a GitHub release. The WASI module is built from source (below) rather than published to a registry.

Requirements: Java 21+ for the JVM and CLI; Node β‰₯ 18 (22+ recommended) or Bun for JavaScript; a WASI runtime with the function-references, gc and exceptions proposals β€” wasmtime 46+ is known good.

Quick start

Command line

java -jar vcyclist-cli-*-all.jar enhance route.gpx --gpx out.gpx --csv out.csv

enhance runs the physics pipeline; export produces maps, FIT, CSV and JSON from a file you already have. Elevation correction is off unless you pass --fix-elevation, so nothing touches the network by default.

Full option reference, the rider models and what each is measured to be worth, and exit codes: cli/README.md.

Kotlin

import io.github.glandais.engine.Enhancer
import io.github.glandais.engine.gpx.GpxParser
import io.github.glandais.engine.gpx.GpxWriter
import io.github.glandais.engine.gpx.firstTrackAsPath
import io.github.glandais.engine.gpx.toGpxDocument

suspend fun virtualize(xml: String): String {
    val path = GpxParser.parse(xml).firstTrackAsPath()
    val out = Enhancer.enhanceCourseDefault(path)   // pure physics, no HTTP
    return GpxWriter.write(out.toGpxDocument(trackName = "virtualized"))
}

Pass an ElevationProvider as the second argument to correct elevations from DEM tiles, and an EnhanceOptions as the third to configure the pipeline. For a configured rider, build a CoursePhysics(Course(path, cyclist, bike), …) and call Enhancer.enhanceCourse.

Java

Path input = GpxToPathJvm.firstTrackAsPath(GpxParserJvm.parse(xml));
Path enhanced = EnhancerJvm.enhanceCourseDefaultBlocking(input);
String out = GpxWriterJvm.write(enhanced);

Every entry point has a …Jvm twin that restores Kotlin's default arguments, and every suspend function has both a …Blocking and a …Async (CompletableFuture) bridge. docs/guides/using-from-java.md has the calling rules β€” some of them matter, …Blocking on a UI thread being the obvious one.

JavaScript / TypeScript

Kotlin/JS emits a UMD bundle that preserves the package namespace, so there is exactly one top-level export. Named imports do not work β€” unwrap it once:

import * as engineRaw from '@glandais/vcyclist-engine';
const engine = engineRaw.io.github.glandais.engine;

const { parseGpx, enhance, writeGpx, pathSize, pathTotalDistance } = engine;

const path = parseGpx(gpxXml);
const out  = await enhance(path, null);          // physics only; { fixElevation: true } for DEM
console.log(pathSize(out), pathTotalDistance(out), 'm');

// `<power>` carries the SOURCE file's power by default β€” writing simulated data into a format the
// ecosystem reads as a recording is the caller's call. Ask for the simulation explicitly:
const xml  = writeGpx(out, true, 'computed-or-input', 'my route');

docs/guides/using-from-javascript.md covers the whole faΓ§ade β€” enhanceWithCourse and its five DTOs, FIT and CSV/JSON export, climbs, the racing-line report, the standalone elevation API, and the Node/Bun specifics.

WASI β€” no JVM, no JavaScript

:engine links a standalone WASI module, so the whole pipeline runs inside wasmtime, WasmEdge, wazero, or an embedding in Go, Rust, Python or the JVM.

./gradlew :engine:wasmModule       # -> engine/build/wasm/vcyclist-engine.wasm + .sha256

The host implements three imports β€” read_input, write_output and fetch_tile (which may simply answer "no tile") β€” and everything else is numeric exports over integer handles:

staged["bytes"] = open("ride.gpx", "rb").read()
handle = exports["vcParseGpx"](store, len(staged["bytes"]))

staged["bytes"] = b'{"computeOnePointPerSecond": true}'
out = exports["vcEnhance"](store, handle, len(staged["bytes"]))
print(exports["vcPathDurationMs"](store, out) / 1000, "s")

This is not a reduced surface: vcEnhanceWithCourse, vcPathToFit, vcPathToCsv, vcDetectClimbsJson and vcAnalyzeRacingLineJson are all there, and Β§10 of the guide is a function-by-function parity table against the JavaScript faΓ§ade. docs/guides/wasm-wasi-abi.md is the full contract; tools/wasi is a working host that CI runs on every pull request.

Try the demo

https://glandais.github.io/vcyclist β€” no install, runs the real engine in your browser.

A Vue 3 + Leaflet + Chart.js app with two routes, both on the same Kotlin/JS bundle:

  • #/ β€” GPX analysis: upload a route, run the physics pipeline, inspect every field on a synchronized chart and map, with climb detection and the racing line, then download the result as GPX or as a Garmin FIT course.
  • #/elevation β€” elevation explorer: query DEM tiles at a point or along a path, with smoothing, Douglas-Peucker simplification and hillshade/slope relief.
cd demo && npm run dev        # http://localhost:3000, against a locally built engine

See demo/README.md for the architecture and the static-site build.

Documentation

docs/README.md is the index and says which documents are current and which are frozen history. The short version:

  • docs/guides/ β€” how to use and extend the project: Java, JavaScript, the WASI ABI, the racing line, the release flow
  • docs/ledgers/ β€” living state: research improvements, build warnings, surface coverage
  • docs/research/ β€” the solo-rider simulation research report
  • Module documentation lives next to its module: cli/, elevation/, map/, demo/

Contributing

Open PRs against develop β€” the default and only long-lived branch β€” using Conventional Commits. Build commands, module layout, testing conventions and the release flow are in CONTRIBUTING.md.

License

Apache License 2.0.

About

Kotlin Multiplatform physics-based cycling simulator: turns GPS traces into virtualized rides with realistic speed, time and power. JVM, JS/npm and WASI.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages