@glandais/vcyclist-engine-wasm
v2.0.0
Published
[](https://www.npmjs.com/package/@glandais/vcyclist-engine) [ 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 (150 m kernel) │
│ ├─ MaxSpeedComputer (cornering+braking)│
│ ├─ VirtualizeService (1 Hz physics) │
│ ├─ PointPerSecond (uniform sampling) │
│ └─ PathSimplifier (Douglas-Peucker 3D) │
└──────────────────┬──────────────────────┘
▼
┌──────────────┐
│ GpxWriter │────▶ output.gpx
└──────────────┘
(*) optional — needs an ElevationProviderModules
| 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 |
| :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 |
| :engine | Physics (4 resistive PowerProviders + 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 |
| :fit | Garmin FIT encoding. FitCourse model + unit conversions in commonMain; expect object FitEncoder with a JVM actual on com.garmin:fit and JS/Wasm actuals on @garmin/fitsdk. | JVM, JS Node, JS browser, Wasm browser |
| :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 for the usage-policy obligations. | JVM only |
| :cli | Command-line tool (picocli). JVM-only, not published as a library — distributed as an executable jar. Replaces gpx2web's gpxtools-cli. | JVM only |
| :codegen | Tiny build-time helper that regenerates GeneratedPath.kt + PointFieldAccessors.kt from PointField (run only when the field list changes). | JVM only |
Migrating from gpx2web
vcyclist replaces the gpx and gpxtools-cli modules of
gpx2web.
docs/gpx2web-coverage.md has one row per Java class — ported,
replaced, or not ported with the reason — plus the deliberate behavioural differences. For the
command-line options specifically, see cli/README.md.
Install
npm (Kotlin/JS or Kotlin/Wasm consumers)
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 bundleGradle / Maven (JVM or KMP consumers)
// 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.
vcyclist-gpx (the Path model + GPX I/O) comes in transitively via vcyclist-engine, and
can also be depended on alone if you only need parsing and resampling.
See docs/publishing.md for the release process.
Quick start
Run the CLI
The CLI is not on Maven Central — it is an application, distributed as a self-contained jar
attached to each GitHub release. Download
vcyclist-cli-<version>-all.jar and run it with Java 21+, or build it yourself:
# Build a self-contained jar, then run it from anywhere:
./gradlew :cli:executableJar
java -jar cli/build/libs/vcyclist-cli-*-all.jar enhance route.gpx --gpx out.gpx --csv out.csv
# Or, during development:
./gradlew :cli:run -Pargs="enhance route.gpx --gpx /tmp/out.gpx"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 usage, exit codes and the migration table from gpx2web's
gpxtools-cli are in cli/README.md.
Try the browser demos (elevation only)
# Kotlin/Wasm demo
./gradlew :elevation:wasmJsBrowserDevelopmentRun
# Kotlin/JS demo (sibling, same UI)
./gradlew :elevation:jsBrowserDevelopmentRunBoth demos share the original TS demo UI (Leaflet +
Chart.js + GPX upload). See elevation/README.md for details.
Use from Kotlin
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(
out.toGpxDocument(trackName = "virtualized")
)
}Use from JavaScript / TypeScript
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.
Getting at the exports
Kotlin/JS emits a UMD bundle that preserves the package namespace, so the module has
exactly one top-level export, io. Named imports do not work — import { parseGpx } from
'@glandais/vcyclist-engine' fails under Node ESM with "Named export 'parseGpx' not found".
Unwrap the namespace once and destructure from it :
import * as engineRaw from '@glandais/vcyclist-engine'; // or: require('@glandais/vcyclist-engine')
const engine = engineRaw.io.github.glandais.engine;Every snippet below assumes that engine. demo/src/engine-shim.ts does the same unwrap and
adds TypeScript types for the DTOs, which Kotlin/JS emits as referenced names only.
Browser
const { parseGpx, enhance, writeGpx, pathSize, pathTotalDistance } = engine;
const handle = parseGpx(gpxXml);
console.log('input points:', pathSize(handle));
const out = await enhance(handle, null); // physics only, no HTTP
console.log('output:', pathSize(out), pathTotalDistance(out), 'm');
const xml = writeGpx(out);Node.js / Bun (with elevation correction)
const { parseGpx, enhance, writeGpx } = 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).
FIT export
const { parseGpx, enhance, pathToFit } = engine;
const out = await enhance(parseGpx(gpxXml), null);
const fit = pathToFit(out, 'My route', Date.parse('2026-08-01T08:00:00Z'));
// Kotlin/JS returns an Int8Array; the -wasm build returns a Uint8Array. Both are the same file.FIT has no relative clock, so the start instant is mandatory. The output is a Course file (a route to follow), which is what a virtualized trace should be — not an Activity.
Multi-track GPX
parseGpx returns the first track, which is what most files contain. For documents with
several <trk> or several <trkseg> :
const { parseGpxTracks, parseGpxSegments, writeGpxTracks } = engine;
const tracks = parseGpxTracks(gpxXml); // one path per <trk>, segments concatenated
const segments = parseGpxSegments(gpxXml); // one path per <trkseg>, always continuous
const xml = writeGpxTracks(tracks); // one <trk> per pathConcatenating segments folds the pause between them into totalDistance — a <trkseg>
boundary is a physical discontinuity. Use parseGpxSegments when that artefact matters.
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 (globalThis.fetch is built-in since Node 18 / Bun) ; Node 22+ recommended for
ESM require() support.
Try the interactive demo
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 serverSee demo/README.md for the dev workflow and architecture.
Build & test
./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 # lintLayout
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/ # :codegen JVM helper for Path accessor generationStatus
- ✅ Phase 1 —
:elevationmodule port (tasks 00-09) : Terrarium tiles, Haversine, ECEF, Douglas-Peucker 3D, smoother, LRU cache + TileManager,ElevationProvider, live HTTP integration. - ✅ Phase 2 —
:enginemodule port (tasks 10-28) : Path model, Cyclist/Bike/Course, GPX I/O, full physics, simulation, post-processing,Enhancer, CLI,@JsExportfaçades. - ✅ Phase 2bis — pipeline fidelity fixes (tasks 29-31) :
VirtualizeServicelast-point timestamp,PointPerDistanceport, integration intoEnhancer. - ✅ Phase 3 — Node.js / Bun support (tasks 32-33) : runtime-detection in
TileFetcher.js.kt(browser path unchanged, Node path usesglobalThis.fetch+@jsquash/webpWASM decoder loaded via lazyeval('require')), webpack externals to keep the browser bundle free of@jsquash/webp,ElevationProviderauto-instantiation inEngineJsApi.enhancewhenopts.fixElevationis true (JS + Wasm façades), 6 jsTest classes gated byINTEGRATION=1.
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.
Documentation
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@JsExportfaçades and the WebP tile decoding.docs/publishing.md— release flow (Maven Central + npm via semantic-release on push todevelop).elevation/README.md—:elevationmodule details + browser demos.
Contributing
develop is the default and only long-lived branch — there is no main. Open PRs
against develop using Conventional Commits :
feat: triggers a minor release, fix: a patch, anything else is a no-op release-wise.
Every push to develop runs the full multi-target test suite via
.github/workflows/release.yml and, if green, lets semantic-release tag a new version,
publish to Maven Central + npm, and commit the version bump back to develop with
[skip ci]. See docs/publishing.md for the full flow.
License
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.
