# Isochrone App > Offline isochrone map explorer β€” Electron desktop app + deployable web SPA, powered by a self-hosted [Valhalla](https://github.com/valhalla/valhalla) routing engine. [![Electron](https://img.shields.io/badge/Electron-39-47848F?logo=electron&logoColor=white)](https://www.electronjs.org/) [![React](https://img.shields.io/badge/React-19-61DAFB?logo=react&logoColor=black)](https://react.dev/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![MapLibre GL](https://img.shields.io/badge/MapLibre_GL-5-396CB2?logo=maplibre&logoColor=white)](https://maplibre.org/) [![Valhalla](https://img.shields.io/badge/Routing-Valhalla-FF6B35?logo=openstreetmap&logoColor=white)](https://github.com/valhalla/valhalla) [![Self-hosted](https://img.shields.io/badge/self--hosted-100%25_offline-22c55e)](https://github.com/DaKerboul/isochrone-app) [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE) ![isochrone demo](https://raw.githubusercontent.com/DaKerboul/isochrone-app/master/resources/screenshot.png) --- ## 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 ```bash npm install ``` ### Build Valhalla tiles Download an OSM extract from [Geofabrik](https://download.geofabrik.de/) and place it in a `valhalla/` directory, then: ```bash 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` (~8–12 GB for France). ### Run Valhalla service ```bash 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 ```bash npm run dev ``` ### Build standalone web SPA ```bash 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: ```json "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): ```yaml 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