No description
Find a file
2026-03-01 21:59:44 +01:00
apps/web initial commit 2026-03-01 21:59:44 +01:00
infra initial commit 2026-03-01 21:59:44 +01:00
shared initial commit 2026-03-01 21:59:44 +01:00
worker initial commit 2026-03-01 21:59:44 +01:00
.env.example initial commit 2026-03-01 21:59:44 +01:00
.gitignore initial commit 2026-03-01 21:59:44 +01:00
docker-compose.yml initial commit 2026-03-01 21:59:44 +01:00
Dockerfile initial commit 2026-03-01 21:59:44 +01:00
package-lock.json initial commit 2026-03-01 21:59:44 +01:00
package.json initial commit 2026-03-01 21:59:44 +01:00
README.md initial commit 2026-03-01 21:59:44 +01:00
tsconfig.base.json initial commit 2026-03-01 21:59:44 +01:00

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

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

docker compose up -d

3. Add a city

Open 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 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:

# 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

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:

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.