fifteen/README.md

156 lines
6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Transportationer — 15-Minute City Analyzer
A web application for analyzing urban accessibility through the lens of the 15-minute city concept. Shows a heatmap indicating distance to locations of interest across 5 categories: **Service & Trade**, **Transport**, **Work & School**, **Culture & Community**, and **Recreation**.
## Architecture
```
Browser (Next.js / React)
├── MapLibre GL JS (map + canvas heatmap / isochrone overlay)
└── API calls → Next.js API routes
Next.js App Server
├── Public API: /api/cities /api/tiles /api/stats /api/location-score /api/isochrones
├── Admin API: /api/admin/** (auth-protected)
├── PostgreSQL + PostGIS (POIs, grid points, precomputed scores)
└── Valkey (API response cache, BullMQ queues)
BullMQ Worker (pipeline queue, concurrency 8)
├── refresh-city → orchestrates full ingest via FlowProducer
├── download-pbf → streams OSM PBF from Geofabrik
├── extract-pois → osmium filter + osm2pgsql flex → raw_pois
├── build-valhalla → clips PBF, builds Valhalla routing tiles
├── generate-grid → PostGIS 200 m hex grid → grid_points
├── compute-scores → two-phase orchestrator (see Scoring below)
└── compute-routing → Valhalla matrix → grid_poi_details
(15 parallel jobs: 3 modes × 5 categories)
BullMQ Worker (valhalla queue, concurrency 1)
└── build-valhalla → runs valhalla_build_tiles, manages valhalla_service
Valhalla (child process of valhalla worker)
├── sources_to_targets matrix → compute-routing jobs
└── isochrones endpoint → user click → /api/isochrones
Protomaps → self-hosted map tiles (PMTiles)
```
## Quick Start
### 1. Configure environment
```bash
cp .env.example .env
# Edit .env with strong passwords
# Generate admin password hash
node -e "require('bcryptjs').hash('yourpassword', 12).then(console.log)"
# Paste result as ADMIN_PASSWORD_HASH in .env
```
### 2. Start services
```bash
docker compose up -d
```
### 3. Add a city
Open [http://localhost:3000/admin](http://localhost:3000/admin), log in, click **Add City**, browse Geofabrik regions (e.g. `europe/germany/berlin`), and start ingestion. Progress is shown live.
Processing time:
- Small city (< 100k pop): ~515 minutes
- Large city (1M+ pop): ~3090 minutes
### 4. Explore
Open [http://localhost:3000](http://localhost:3000) and select your city.
## Map Tiles
By default the app uses CartoDB Positron (CDN). For fully offline operation, download a PMTiles file for your region:
```bash
# Example: download Berlin region tiles
wget https://maps.protomaps.com/builds/berlin.pmtiles -O apps/web/public/tiles/region.pmtiles
# Then switch to the PMTiles style:
cp apps/web/public/tiles/style.pmtiles.json apps/web/public/tiles/style.json
```
## Development
```bash
npm install
npm run dev # Next.js dev server on :3000
npm run worker:dev # BullMQ worker with hot reload
```
Required local services: PostgreSQL+PostGIS, Valkey. Easiest via:
```bash
docker compose up postgres valkey -d
```
## Category Definitions
| Category | OSM Sources |
|----------|-------------|
| Service & Trade | supermarkets, shops, restaurants, pharmacies, banks, cafés |
| Transport | bus stops, metro, tram, train stations, bike share, car share |
| Work & School | offices, coworking, schools, kindergartens, universities |
| Culture & Community | libraries, hospitals, clinics, museums, theatres, community centres |
| Recreation | parks, playgrounds, sports centres, gyms, green spaces |
## Scoring
### Data pipeline
For each grid point (200 m hexagonal spacing) the pipeline runs in two phases:
**Phase 1 — Routing** (15 parallel jobs: 3 modes × 5 categories)
A PostGIS KNN lateral join (`<->` operator) finds the 6 nearest POIs in the category for each grid point. Those POI coordinates are passed to Valhalla's `sources_to_targets` matrix API to obtain real network travel times for the requested travel mode (walking, cycling, driving). The nearest POI per subcategory is persisted to `grid_poi_details`.
**Phase 2 — Score aggregation**
Scores are precomputed for every combination of:
- 5 thresholds: 5, 10, 15, 20, 30 minutes
- 3 travel modes: walking, cycling, driving
- 5 profiles: Universal, Young Family, Senior, Young Professional, Student
### Scoring formula
Each subcategory *i* within a category contributes a sigmoid score:
```
sigmoid(t, T) = 1 / (1 + exp(4 × (t T) / T))
```
Where `t` is the Valhalla travel time in seconds and `T` is the threshold in seconds. The sigmoid equals 0.5 exactly at the threshold and approaches 1 for very short times.
The category score combines subcategories via a complement-product, weighted by per-profile subcategory importance weights `w_i ∈ [0, 1]`:
```
category_score = 1 ∏ (1 w_i × sigmoid(t_i, T))
```
This captures diversity of coverage: reaching one supermarket near you already yields a high score, but having a pharmacy, bakery, and bank nearby as well pushes the score higher.
### Profiles
Each profile carries two sets of weights:
- **Category weights** (used as slider presets in the UI, range 02): how much relative importance each of the 5 categories gets in the composite score.
- **Subcategory weights** (used during score computation, range 01): how much a specific subcategory contributes to its category score.
| Profile | Focus |
|---------|-------|
| Universal | Balanced across all resident types |
| Young Family | Schools, playgrounds, healthcare, daily shopping |
| Senior | Healthcare, local services, accessible green space, transit |
| Young Professional | Rapid transit, fitness, dining, coworking |
| Student | University, library, cafés, transit, budget services |
### Composite score
The composite shown on the heatmap is a weighted average of the 5 category scores. Category weights come from the selected profile but can be adjusted freely in the UI. All scores are precomputed — changing the profile or weights only triggers a client-side re-blend with no server round-trip.