105 lines
3.5 KiB
Markdown
105 lines
3.5 KiB
Markdown
# 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)
|
||
└── API calls → Next.js API routes
|
||
|
||
Next.js App Server
|
||
├── Public API: /api/cities /api/pois /api/grid /api/stats /api/isochrones
|
||
├── Admin API: /api/admin/** (auth-protected)
|
||
├── PostgreSQL + PostGIS (POI data, grid scores, isochrone cache)
|
||
└── Valkey (API response cache, sessions, BullMQ queue)
|
||
|
||
BullMQ Worker (separate process)
|
||
├── download-pbf → streams OSM data from Geofabrik
|
||
├── extract-pois → osmium-tool + osm2pgsql → PostGIS
|
||
├── generate-grid → PostGIS SQL (200m grid)
|
||
├── compute-scores → KNN lateral join + sigmoid scoring
|
||
└── build-valhalla → Valhalla routing tile build
|
||
|
||
Valhalla → local routing (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): ~5–15 minutes
|
||
- Large city (1M+ pop): ~30–90 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 | Default Threshold |
|
||
|----------|-------------|-------------------|
|
||
| Service & Trade | shops, restaurants, pharmacies, banks | 10 min |
|
||
| Transport | bus stops, metro, train, bike share | 8 min |
|
||
| Work & School | offices, schools, universities | 20 min |
|
||
| Culture & Community | libraries, hospitals, museums, community centers | 15 min |
|
||
| Recreation | parks, sports, gyms, green spaces | 10 min |
|
||
|
||
## Scoring
|
||
|
||
For each grid point (200m spacing), the nearest POI in each category is found using a PostGIS KNN lateral join. The Euclidean distance is converted to travel time using mode speed assumptions (walking 5 km/h, cycling 15 km/h, driving 40 km/h). A sigmoid function converts travel time to a score in [0,1]:
|
||
|
||
```
|
||
score = 1 / (1 + exp(k * (travel_time - threshold)))
|
||
```
|
||
|
||
Where `k = 4/threshold`, giving score=0.5 exactly at the threshold.
|
||
|
||
The composite score is a weighted average of all 5 category scores, with user-adjustable weights.
|