@jansoft/mbujkanji-valhalla-wasm
v0.1.1
Published
Valhalla routing engine compiled to WebAssembly for offline-first web applications
Maintainers
Readme
@jansoft/mbujkanji-valhalla-wasm
A standalone, framework-agnostic NPM package that brings Valhalla routing engine to the browser via WebAssembly.
Features
- Offline-first routing - Calculate routes entirely client-side without network requests
- Framework agnostic - Works with React, Vue, Svelte, vanilla JS, or any JavaScript environment
- BYOT (Bring Your Own Tiles) - No pre-built tiles included; load your own region tiles
- TypeScript support - Full type definitions included
- Tree-shakeable - Import only what you need
Installation
```bash npm install @jansoft/mbujkanji-valhalla-wasm
or
yarn add @jansoft/mbujkanji-valhalla-wasm
or
pnpm add @jansoft/mbujkanji-valhalla-wasm ```
Quick Start
```typescript import { createRouter } from '@jansoft/mbujkanji-valhalla-wasm';
// Create and initialize the router (WASM paths auto-detected!) const router = createRouter(); await router.init(); // No paths needed - it just works!
// Load tiles (you provide these) const tilesResponse = await fetch('/path/to/your/tiles.tar'); const tilesBuffer = await tilesResponse.arrayBuffer(); await router.loadTiles(tilesBuffer);
// Calculate a route const result = await router.route({ locations: [ { lat: 4.0511, lon: 9.7679 }, // Origin (Douala) { lat: 3.8480, lon: 11.5021 } // Destination (Yaounde) ], costing: 'auto', directions_type: 'maneuvers' });
console.log(result.trip);
// Clean up when done router.dispose(); ```
Installation & Setup
Automatic Path Resolution (Default)
The package automatically detects WASM file paths based on your environment. No configuration needed!
```typescript // This just works in most environments const router = createRouter(); await router.init(); ```
Works automatically in:
- ✅ Vite
- ✅ Webpack
- ✅ Next.js
- ✅ Node.js (ESM and CJS)
- ✅ Browser (with bundlers)
- ✅ Most modern build tools
Custom Paths (Advanced)
For CDN usage or custom locations, you can provide explicit paths:
```typescript // Custom paths from CDN await router.init({ wasmPath: 'https://cdn.example.com/valhalla.wasm', jsGluePath: 'https://cdn.example.com/valhalla.js' });
// Or inspect auto-detected paths import { getWasmPaths } from '@jansoft/mbujkanji-valhalla-wasm'; const paths = getWasmPaths(); console.log('WASM:', paths.wasm); console.log('JS:', paths.js); ```
Framework-Specific Setup
Vite
No special configuration needed! The package works out of the box.
```typescript // vite.config.ts - No changes needed import { defineConfig } from 'vite'
export default defineConfig({ // Package handles WASM automatically }) ```
Next.js
Works automatically with both App Router and Pages Router.
```typescript // app/page.tsx or pages/index.tsx 'use client' // If using App Router
import { createRouter } from '@jansoft/mbujkanji-valhalla-wasm'
export default async function Page() { const router = createRouter() await router.init() // Auto-detects paths // ... } ```
Webpack
No special configuration needed. Webpack handles WASM files automatically.
Node.js
For server-side usage, ensure Node.js 18+ (has native fetch) or provide a fetch polyfill:
```typescript // Node.js 18+ import { createRouter } from '@jansoft/mbujkanji-valhalla-wasm' const router = createRouter() await router.init() // Works with native fetch
// Node.js < 18 (with polyfill) import fetch from 'node-fetch' const router = createRouter() await router.init({ fetchFn: fetch }) ```
API Reference
createRouter(options?: RouterConfig): ValhallaRouter
Factory function to create a new router instance.
```typescript interface RouterConfig { defaultCosting?: 'auto' | 'bicycle' | 'pedestrian' | 'truck'; defaultUnits?: 'kilometers' | 'miles'; defaultLanguage?: string; verbose?: boolean; // Enable debug logging } ```
ValhallaRouter
init(options?: ValhallaInitOptions): Promise<void>
Initialize the WASM module. Must be called before any other methods.
Options (all optional - paths are auto-detected by default):
```typescript interface ValhallaInitOptions { wasmPath?: string; // Custom WASM file path (auto-detected if not provided) jsGluePath?: string; // Custom JS glue path (auto-detected if not provided) fetchFn?: typeof fetch; // Custom fetch function (for Node.js < 18 or custom auth) onProgress?: (progress: LoadProgress) => void; // Progress callback onError?: (error: Error) => void; // Error callback onReady?: () => void; // Ready callback } ```
Example: ```typescript // Auto-detection (recommended) await router.init();
// With progress tracking
await router.init({
onProgress: (p) => console.log(${p.phase}: ${p.percent}%)
});
// Custom paths await router.init({ wasmPath: 'https://cdn.example.com/valhalla.wasm', jsGluePath: 'https://cdn.example.com/valhalla.js' }); ```
loadTiles(tiles: ArrayBuffer): Promise<void>
Load routing tiles from an ArrayBuffer (typically a .tar file).
loadTilesFromUrl(url: string): Promise<void>
Convenience method to fetch and load tiles from a URL.
route(request: RouteRequest): Promise<RouteResponse>
Calculate a route between locations.
```typescript interface RouteRequest { locations: Array<{ lat: number; lon: number; type?: 'break' | 'through' | 'via'; heading?: number; street?: string; }>; costing: 'auto' | 'bicycle' | 'pedestrian' | 'truck'; costing_options?: CostingOptions; directions_type?: 'none' | 'maneuvers' | 'instructions'; units?: 'kilometers' | 'miles'; language?: string; alternates?: number; } ```
isReady(): boolean
Check if the router is initialized and has tiles loaded.
getLoadedRegions(): string[]
Get list of loaded tile regions.
dispose(): void
Clean up resources. Call when done with the router.
Utilities
```typescript import { decodePolyline, encodePolyline, polylineToGeoJSON, haversineDistance, bearing } from '@jansoft/mbujkanji-valhalla-wasm';
// Decode Valhalla's encoded polyline const coordinates = decodePolyline(encodedString, 6);
// Convert to GeoJSON const lineString = polylineToGeoJSON(encodedString, 6);
// Calculate distance between two points (meters) const distance = haversineDistance( { lat: 4.0511, lon: 9.7679 }, { lat: 3.8480, lon: 11.5021 } ); ```
Optional: Tile Caching
For offline-first applications, use the optional cache module:
```typescript import { TileCache } from '@jansoft/mbujkanji-valhalla-wasm/cache';
const cache = new TileCache({ dbName: 'my-app-tiles' }); await cache.init();
// Store tiles await cache.storeTiles('cameroon', tilesBuffer, { version: '2024-01', bounds: { north: 13.1, south: 1.6, east: 16.2, west: 8.5 } });
// Retrieve tiles const tiles = await cache.getTiles('cameroon');
// List cached regions const regions = await cache.listRegions(); ```
Building WASM from Source
The package requires a compiled valhalla.wasm file. See native/README.md for build instructions.
Quick Build (Docker)
From repo root (recommended; writes to wasm/):
```bash pnpm run build:wasm ```
Or manually (mount must be /artifacts so the container can write the built files):
```bash cd native docker build -t valhalla-wasm-builder . docker run --rm -v $(pwd)/../wasm:/artifacts valhalla-wasm-builder ```
Then run pnpm run build to build the package. Both wasm/valhalla.wasm and wasm/valhalla.js are required.
Creating Routing Tiles
Valhalla requires routing tiles built from OpenStreetMap data. Here's how to create tiles for your region:
Option 1: Use Valhalla Docker Image
```bash
Download OSM extract (example: Cameroon)
wget https://download.geofabrik.de/africa/cameroon-latest.osm.pbf
Build tiles using official Valhalla Docker image
docker run -it --rm
-v $(pwd):/data
ghcr.io/valhalla/valhalla:latest
valhalla_build_tiles -c /data/valhalla.json /data/cameroon-latest.osm.pbf
Package tiles for web delivery
tar -cvf cameroon-tiles.tar valhalla_tiles/ ```
Option 2: Build Locally
See Valhalla documentation for local build instructions.
Examples
See the examples/ directory for complete examples:
- vanilla-js - Plain JavaScript with MapLibre GL JS
- react-maplibre - React with MapLibre GL JS and offline support
Browser Support
- Chrome 89+
- Firefox 89+
- Safari 15+
- Edge 89+
Requires WebAssembly and SharedArrayBuffer support. For SharedArrayBuffer, ensure proper COOP/COEP headers:
``` Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ```
Bundle Size
| Component | Size (gzipped) | |-----------|----------------| | Core library | ~15 KB | | WASM module | ~8 MB | | Worker | ~5 KB |
Optimization Tips
- Lazy Loading: Load WASM only when needed
- CDN: Serve WASM files from a CDN for better caching
- Compression: Ensure your server serves WASM with gzip/brotli compression
- Code Splitting: Use dynamic imports to split WASM loading from main bundle
```typescript // Lazy load WASM const loadRouter = async () => { const { createRouter } = await import('@jansoft/mbujkanji-valhalla-wasm') const router = createRouter() await router.init() return router } ```
Troubleshooting
WASM File Not Found
Error: Failed to fetch WASM: 404 Not Found
Solutions:
- Ensure WASM files are built:
npm run build:wasm - Check that WASM files are in
dist/after runningnpm run build - Verify the package was installed correctly
- For custom paths, ensure the URL is accessible
CORS Errors
Error: CORS policy or cross-origin errors
Solutions:
- Use a bundler (Vite, Webpack) - they handle WASM files automatically
- Copy WASM files to your
public/folder and reference them there - Configure your server to send proper CORS headers
- Use a CDN that supports CORS
Monorepo Issues
Problem: Path resolution fails in pnpm/yarn workspaces
Solutions:
- Ensure the package is properly hoisted
- Use explicit paths if auto-detection fails: ```typescript await router.init({ wasmPath: new URL('valhalla.wasm', import.meta.url).href }) ```
- Check that
node_modulesstructure is correct
Worker Thread Usage
Note: Web Workers have different path resolution behavior. The package handles this automatically, but if you encounter issues:
```typescript // In a Web Worker import { createRouter, getWasmPaths } from '@jansoft/mbujkanji-valhalla-wasm'
// Get paths explicitly for workers const paths = getWasmPaths() const router = createRouter() await router.init({ wasmPath: paths.wasm, jsGluePath: paths.js }) ```
SSR/SSG Initialization
For Next.js Server Components or SSR:
```typescript // Only initialize on client side 'use client'
import { useEffect, useState } from 'react' import { createRouter } from '@jansoft/mbujkanji-valhalla-wasm'
export function MyComponent() { const [router, setRouter] = useState(null)
useEffect(() => { // Initialize only in browser const initRouter = async () => { const r = createRouter() await r.init() setRouter(r) } initRouter() }, [])
// ... } ```
Path Resolution Errors
Error: import.meta.url is not defined (in CJS)
Solution: The package handles this automatically. If you see this error:
- Ensure you're using the latest version
- Check that your bundler is configured correctly
- Use ESM instead of CJS if possible
Common Path Resolution Errors
- Wrong path format: Use URLs or absolute paths, not relative paths from root
- Bundler not copying WASM: Ensure your bundler is configured to handle
.wasmfiles - Node.js path issues: Use
file://URLs or absolute paths in Node.js
License
MIT License - see LICENSE for details.
Valhalla is licensed under the MIT License.
Contributing
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
Acknowledgments
- Valhalla - The routing engine
- Emscripten - C++ to WebAssembly compiler
- MapLibre GL JS - Map rendering library
