- 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>
7.1 KiB
AGENT.md — Isochrone App
Architecture and development guide for AI agents working on this codebase.
What this app does
Renders isochrone maps (reachable-area polygons by travel time) on top of a dark MapLibre map. The user clicks a point, picks a transport mode and time ranges, and the app calls a local Valhalla instance to compute the polygons.
Build targets
| Target | Command | Output | Entry |
|---|---|---|---|
| Electron desktop | npm run dev / npm run build |
out/ |
src/main/index.ts |
| Web SPA | npm run build:web |
dist-web/ |
src/renderer/index.html |
The renderer (src/renderer/) is shared between both targets. The only difference is how Valhalla is called:
- Electron: via IPC (
window.api.valhallaFetch) → main process proxies with Node.jshttp - Web: via native
fetch()directly toVITE_VALHALLA_URL
The adapter lives entirely in src/renderer/src/api/ors.ts.
Project layout
src/
main/
index.ts # Electron main process
# - starts/stops Valhalla Docker container (WSL)
# - IPC handlers: valhalla-fetch, valhalla-abort, valhalla-status
# - Node.js http proxy (bypasses Chromium for localhost)
preload/
index.ts # contextBridge: exposes window.api to renderer
index.d.ts # TypeScript types for window.api
renderer/
index.html # SPA entry point
src/
main.tsx # React root
App.tsx # Root component: Valhalla probe, auto-recalculate, splash/error
env.d.ts # ImportMetaEnv: VITE_VALHALLA_URL
api/
ors.ts # Valhalla fetch, color palettes, polygon simplification (RDP)
store/
useAppStore.ts # Zustand store (all global state)
components/
Map.tsx # MapLibre map, isochrone layers, hover/click/contextmenu
ControlPanel.tsx # Left panel UI: mode, geocoder, time ranges, history, error
TimeRangeEditor.tsx # Add/remove time ranges, preset chips
Legend.tsx # Interactive layer legend (toggle visibility)
Toast.tsx # Toast notification system
MapOverlay.tsx # Loading bar overlay
Versions.tsx # Electron version display (unused in web build)
utils/
geocoder.ts # Nominatim search (fetch, no API key)
export.ts # GeoJSON / PNG export
assets/
main.css # All app styles (dark theme, components)
base.css # CSS reset / root variables
vite.web.config.ts # Vite config for standalone SPA build (no Electron)
electron.vite.config.ts # electron-vite config (main + preload + renderer)
State (Zustand — useAppStore.ts)
| Field | Type | Description |
|---|---|---|
point |
[number, number] | null |
Selected map point [lng, lat] |
mode |
TransportMode |
'auto' | 'bicycle' | 'pedestrian' |
timeRanges |
number[] |
Array of seconds (e.g. [900, 1800, 3600]) |
isochrones |
FeatureCollection | null |
Current computed isochrones |
loading |
boolean |
Fetch in progress |
error |
string | null |
Last error message |
valhallaStatus |
'starting' | 'ready' | 'error' |
Routing engine status |
toasts |
Toast[] |
Active toast notifications |
history |
HistoryEntry[] |
Last 5 calculations (max) |
hiddenLayers |
Set<number> |
isoIndex values of hidden layers |
autoRecalculate |
boolean |
Auto-recompute on param change |
Data flow
User click on map
→ Map.tsx sets point in store
→ ControlPanel.tsx calls fetchIsochrones()
→ ors.ts: builds Valhalla request body
→ Electron: window.api.valhallaFetch() → IPC → Node http.request → Valhalla
Web: fetch(VITE_VALHALLA_URL + '/isochrone')
→ response: raw GeoJSON FeatureCollection
→ ors.ts: reverses features (outermost first), stamps isoColor/isoOpacity/isoIndex/isoLabel, RDP-simplifies polygons
→ store.setIsochrones(data)
→ Map.tsx useEffect([isochrones]): updates GeoJSON source, animates opacity, places label markers
→ Legend.tsx: renders color swatches from getIsochroneColors(mode)
Valhalla integration
Request format
POST /isochrone
{
"locations": [{ "lon": 2.3488, "lat": 48.8534 }],
"costing": "auto",
"contours": [{ "time": 15 }, { "time": 30 }, { "time": 60 }],
"polygons": true,
"denoise": 0.5,
"generalize": 150
}
Response
GeoJSON FeatureCollection. Each feature has properties.contour (time in minutes).
Limits (configured in valhalla.json)
"service_limits": {
"isochrone": {
"max_contours": 8,
"max_time_contour": 600
}
}
Color palettes (per transport mode)
Defined in ors.ts — ISOCHRONE_COLORS: Record<TransportMode, string[]>. Export getIsochroneColors(mode) used by Map, Legend, TimeRangeEditor.
Map layers (MapLibre)
| Layer ID | Type | Source | Description |
|---|---|---|---|
iso-fill |
fill | isochrones GeoJSON |
Isochrone polygons, opacity from isoOpacity feature property |
iso-line |
line | isochrones GeoJSON |
Isochrone outlines |
point-dot |
circle | point GeoJSON |
Departure point marker |
Feature-state hovered: true on iso-fill raises opacity to 0.6. Labels are DOM Marker elements with class iso-map-label.
Layer visibility is controlled via map.setFilter(LAYER_FILL, ['in', ['get','isoIndex'], ['literal', visibleIndices]]) driven by store.hiddenLayers.
CSS conventions
All styles in src/renderer/src/assets/main.css. CSS variables defined on :root:
| Variable | Value | Usage |
|---|---|---|
--bg |
#0f111a |
App background |
--panel-bg |
#161926 |
Panel background |
--accent |
#6366f1 |
Primary accent (indigo) |
--danger |
#f87171 |
Error states |
--text |
#e2e8f0 |
Primary text |
--text-muted |
#64748b |
Secondary text |
--radius |
8px |
Standard border radius |
Adding a new transport mode
- Add key to
TransportModeunion inuseAppStore.ts - Add entry to
COSTING_MAPinors.ts(Valhalla costing string) - Add color palette to
ISOCHRONE_COLORSinors.ts - Add button in
ControlPanel.tsxmode selector withdata-modeattribute - Add
.btn-mode.active[data-mode="..."]style inmain.css
Environment
- Node.js 20+
- Valhalla:
ghcr.io/gis-ops/docker-valhalla/valhalla:latest - Tiles built from Geofabrik
.osm.pbfextracts - In Electron mode: Docker runs in WSL2 Ubuntu, port-forwarded to
127.0.0.1:8002 - In web mode:
VITE_VALHALLA_URLenv var (injected at build time by Vite)
Known constraints
VITE_VALHALLA_URLis build-time, not runtime — rebuild needed to change the Valhalla URL- Electron app assumes WSL2 Ubuntu with Docker at
/home/kerboul/valhalla/ - CartoCDN tiles require internet access (map background only — routing is offline)
Versions.tsxuseswindow.electron— renders nothing in web build (unused component)