Files
isochrone-app/AGENT.md
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

201 lines
7.1 KiB
Markdown

# 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.js `http`
- Web: via native `fetch()` directly to `VITE_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
```json
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`)
```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
1. Add key to `TransportMode` union in `useAppStore.ts`
2. Add entry to `COSTING_MAP` in `ors.ts` (Valhalla costing string)
3. Add color palette to `ISOCHRONE_COLORS` in `ors.ts`
4. Add button in `ControlPanel.tsx` mode selector with `data-mode` attribute
5. Add `.btn-mode.active[data-mode="..."]` style in `main.css`
---
## Environment
- Node.js 20+
- Valhalla: `ghcr.io/gis-ops/docker-valhalla/valhalla:latest`
- Tiles built from Geofabrik `.osm.pbf` extracts
- In Electron mode: Docker runs in WSL2 Ubuntu, port-forwarded to `127.0.0.1:8002`
- In web mode: `VITE_VALHALLA_URL` env var (injected at build time by Vite)
---
## Known constraints
- `VITE_VALHALLA_URL` is **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.tsx` uses `window.electron` — renders nothing in web build (unused component)