# 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.