kerboul 0c9faeb6f3 feat: web SPA build target + UX overhaul + public deployment setup
- Replace Electron IPC with native fetch + AbortController (VITE_VALHALLA_URL)
- Add vite.web.config.ts for standalone SPA build (npm run build:web)
- Per-mode color palettes (green/cyan/amber), RDP polygon simplification
- Interactive legend, hover popup, right-click context menu, map labels
- Toast notifications, history (last 5), auto-recalculate toggle
- Status dot, error card with retry, timing feedback, copy coordinates
- Zustand: toasts, history, hiddenLayers, autoRecalculate
- Add README.md and AGENT.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 14:03:34 +02:00

Isochrone App

Offline isochrone map explorer — Electron desktop app + deployable web SPA, powered by a self-hosted Valhalla routing engine.

isochrone demo


What is this?

An isochrone map shows you all the places you can reach within a given time from a starting point. This app lets you:

  • Click anywhere on the map to set a departure point
  • Choose a transport mode (🚗 car / 🚲 bike / 🚶 walking)
  • Configure up to 8 time ranges (e.g. 15min, 30min, 1h, 2h, 4h, 8h...)
  • See the reachable areas rendered as colored polygons on a dark map

Everything runs 100% offline and self-hosted — no external routing API, no data leaves your infrastructure.


Features

  • Offline-first — Valhalla routing engine runs locally (Docker / WSL / Proxmox CT)
  • Dark map — CartoCDN dark tiles
  • Per-mode color palettes — green (walking) / cyan (bike) / amber (car)
  • Interactive legend — click to toggle individual isochrone layers
  • Hover popup — hover an isochrone to see its duration
  • Right-click context menu — set departure point from map
  • Geocoder — search for a place by name (Nominatim/OpenStreetMap)
  • Export — save as GeoJSON or PNG
  • History — last 5 searches, one-click restore
  • Auto-recalculate — toggle to recompute automatically on param changes
  • Toast notifications — timing feedback, copy coordinates
  • Abort in-flight requests — start a new calculation without waiting
  • Dual build target — Electron desktop app + standalone web SPA (npm run build:web)

Architecture

┌─────────────────────────────────────┐
│           Electron Main             │
│  - Manages Valhalla Docker container│
│  - Proxies HTTP via Node.js         │
│  - IPC bridge (preload)             │
└────────────────┬────────────────────┘
                 │ IPC (Electron) / fetch (Web)
┌────────────────▼────────────────────┐
│         React Renderer (SPA)        │
│  MapLibre GL  │  Zustand store      │
│  ControlPanel │  TimeRangeEditor    │
│  Legend       │  Toast / MapOverlay │
└────────────────┬────────────────────┘
                 │ HTTP POST /isochrone
┌────────────────▼────────────────────┐
│        Valhalla Routing Engine      │
│  ghcr.io/gis-ops/docker-valhalla    │
│  OSM tiles built from Geofabrik PBF │
└─────────────────────────────────────┘

Key files

Path Role
src/main/index.ts Electron main: Docker lifecycle, IPC handlers, HTTP proxy
src/preload/index.ts Electron preload: exposes window.api to renderer
src/renderer/src/api/ors.ts Valhalla fetch logic, color palettes, polygon simplification
src/renderer/src/store/useAppStore.ts Zustand global state
src/renderer/src/components/Map.tsx MapLibre map, layers, hover, context menu, labels
src/renderer/src/components/ControlPanel.tsx Left panel: mode, geocoder, time ranges, history
src/renderer/src/components/TimeRangeEditor.tsx Time range chips + presets
src/renderer/src/components/Legend.tsx Interactive layer legend
vite.web.config.ts Vite config for standalone web SPA build

Stack

  • Frontend — React 19, TypeScript, Vite
  • Map — MapLibre GL v5
  • State — Zustand v5
  • Desktop shell — Electron 39
  • Routing engine — Valhalla (self-hosted via Docker)
  • Map tiles — CartoCDN dark (raster)
  • Geocoding — Nominatim (OpenStreetMap)

Getting started

Prerequisites

  • Node.js 20+
  • Docker (for running Valhalla locally)
  • WSL2 with Ubuntu (if on Windows)

Install

npm install

Build Valhalla tiles

Download an OSM extract from Geofabrik and place it in a valhalla/ directory, then:

docker run --rm \
  -v ./valhalla:/custom_files \
  -e build_tar=True \
  -e serve_tiles=False \
  -e concurrency=8 \
  ghcr.io/gis-ops/docker-valhalla/valhalla:latest \
  build_tiles

This produces valhalla/valhalla_tiles.tar (~812 GB for France).

Run Valhalla service

docker run -d \
  --name valhalla-service \
  -p 8002:8002 \
  -v ./valhalla:/custom_files \
  --entrypoint /usr/local/bin/valhalla_service \
  ghcr.io/gis-ops/docker-valhalla/valhalla:latest \
  /custom_files/valhalla.json 4

Run the Electron app

npm run dev

Build standalone web SPA

VITE_VALHALLA_URL=https://your-valhalla-instance.example.com npm run build:web
# Output in dist-web/

Set VITE_VALHALLA_URL to your public Valhalla endpoint. The SPA calls it directly from the browser (CORS must be enabled on the Valhalla side).


Configuration

Valhalla limits

Edit valhalla/valhalla.json to increase default limits:

"service_limits": {
  "isochrone": {
    "max_contours": 8,
    "max_time_contour": 600
  }
}

Environment variables (web SPA)

Variable Default Description
VITE_VALHALLA_URL http://127.0.0.1:8002 Base URL of Valhalla service

Self-hosted deployment

This app is designed to run self-hosted. The recommended setup:

Browser → isochrone.example.com  (static SPA via CDN / Coolify / Nginx)
               ↓ fetch
          valhalla.example.com  (Traefik → Valhalla Docker on a Proxmox CT)

Traefik config example (restrict CORS + rate limit):

middlewares:
  cors-isochrone:
    headers:
      accessControlAllowOriginList: ["https://isochrone.example.com"]
      accessControlAllowMethods: [GET, POST, OPTIONS]
      accessControlAllowHeaders: [Content-Type]
  valhalla-ratelimit:
    rateLimit:
      average: 20
      burst: 10
      period: 1m

License

MIT

Description
Desktop isochrone map app - zones atteignables par route depuis un point
Readme 1.8 MiB
Languages
TypeScript 63%
CSS 34.1%
JavaScript 2.4%
HTML 0.5%