- 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>
205 lines
6.3 KiB
Markdown
205 lines
6.3 KiB
Markdown
# 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.
|
||
|
||

|
||
|
||
---
|
||
|
||
## 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
|