Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Visited-places world map

An interactive world map (zoom/pan) that plots a set of cities, each rendered as a star (green by default, or a per-city/per-country color set in the input) and clustered when zoomed out. Click a star for the city name; click a cluster to zoom in. It opens on the home city rather than the whole globe. When unlocked with a password (the top-right lock control), each star also shows that city's trip log — dates, activities, and comments — kept encrypted otherwise, and the map moves to the most recent visit (unless a popup is open, which it leaves alone).

The project is generic: it reads cities from an input file and displays them. The current use case is "places I've visited," but it works identically for any city list (e.g. every city with a Burger King).

How it works

Two parts:

  1. A small Python build step geocodes the input into a GeoJSON file.
  2. A static Mapbox GL JS web app renders that GeoJSON.
web/data/visited.json  ──(scripts/build_geojson.py)──>  web/data/places.geojson  ──> map

Input

web/data/visited.json — a list of countries, each with cities. A city may carry an optional intermediate region level (state / province / etc.) used for display and to disambiguate geocoding, and an optional color (also settable on the country, applying to all its cities) overriding the default green star. Producing this file is out of scope here; the committed visited.json holds the actual visited-places list.

[
  { "country": "Japan", "cities": [
    { "name": "Tokyo" },
    { "name": "Sapporo", "region": "Hokkaido" }
  ] }
]

A parenthetical suffix is stripped before geocoding only ("Bruges (Brugge)" is geocoded as "Bruges" but still displayed in full).

See docs/input-format.md for the full schema, field semantics, and geocode-cache details.

Per-city trip details (dates, activities, comments) are not part of visited.json — they live in a separate, encrypted, password-gated source, shown on the map only after the owner enters a password. See Enriched visit log.

Build

python -m venv .venv
# Windows:        .venv\Scripts\activate
# macOS / Linux:  source .venv/bin/activate
pip install -r scripts/requirements.txt
python scripts/build_geojson.py

This geocodes each city via Nominatim (free, no key, rate-limited to 1 request/sec) and writes:

  • web/data/coords_cache.json{ "City, Region, Country": [lng, lat] }, committed and hand-editable: if Nominatim places a city wrong, fix its coordinate here and re-run. Cached queries are not re-fetched, so re-runs are instant.
  • web/data/places.geojson — one Point feature per city, consumed by the web app.

Run locally

./scripts/dev.sh

Then open http://localhost:8000/. The script regenerates the gitignored web/config.js from Doppler (travel-map/dev) before serving, so a fresh clone never comes up with a tokenless, blank map, and it refuses to start if something already holds the port.

Check changes here rather than by deploying — production is a direct upload with no staging environment, so every deploy is a live change to the public site.

The port is 8000 exactly, not a default: the dev Mapbox token is URL-restricted to http://localhost:8000, and Mapbox restrictions take no wildcards or port ranges. On any other port the map renders blank. By hand the equivalent is python -m http.server 8000 --directory web, which skips the token refresh.

Fetching the GeoJSON needs http://, not file://, so opening index.html directly won't work.

Base maps

The base map uses Mapbox, so it needs a free access token. ./scripts/dev.sh writes web/config.js from Doppler (travel-map/dev) on every run, so normally there is nothing to do here. Without Doppler, get a token (Tokens) and paste it in by hand:

cp web/config.example.js web/config.js   # then paste your token into config.js

The map renders a single, customized Mapbox Streets globe — roads hidden and country/region borders sharpened. Without a token the app shows a prompt instead of a map.

web/config.js is gitignored. You can override the style with any Mapbox style URL via window.MAP_STYLE (which also skips the Streets-specific customizations).

Project layout

.
├── scripts/
│   ├── requirements.txt
│   ├── build_geojson.py     # writes into web/data/
│   ├── build_site.mjs       # stages web/ → dist/travel/ for deploy
│   ├── dev.sh               # local server on :8000 (refreshes web/config.js first)
│   ├── crypto/              # shared PBKDF2 + AES-GCM envelope (Node ESM)
│   ├── visits-encrypt.mjs   # visits.source.json → visits.enc
│   └── visits-decrypt.mjs   # visits.enc → visits.source.json (fresh clone)
└── web/                     # the deployable bundle
    ├── index.html           # loads Mapbox GL JS from CDN
    ├── app.js               # star layer, star color, popups, lock control, Streets tweaks
    ├── crypto.js            # in-browser decrypt of visits.enc (mirror of scripts/crypto)
    ├── style.css
    ├── config.example.js    # template for the Mapbox token
    └── data/
        ├── visited.json      # INPUT: countries → cities (+ optional region, color)
        ├── coords_cache.json # geocode cache (committed, hand-editable)
        ├── ranks.json        # label-prominence cache (committed, hand-editable)
        ├── places.geojson    # generated; consumed by the web app
        └── visits.enc        # encrypted trip log (committed, shipped; in-browser unlock)

Hosting

The site is published to Cloudflare Pages under a /travel subpath, by CI. Pushing to main runs .github/workflows/publish.yml, which builds the bundle, uploads it, and then verifies the live site is actually serving that build.

Publishing from a developer machine is deliberately not the path. A local upload ships the working tree while Wrangler stamps the deployment with local HEAD, so uncommitted work can go live under a commit hash that doesn't describe it — and picking the wrong secrets environment bakes a non-production token into the bundle, which fails only in production. CI builds a clean checkout of one commit with one pinned set of credentials, and neither is possible.

Secrets live in Doppler (project travel-map), injected via doppler run so nothing sensitive is written to disk:

Config Holds Used by
dev MAPBOX_TOKEN (restricted to http://localhost:8000), TRAVEL_VISITS_PASSWORD local work
prd MAPBOX_TOKEN (restricted to the live domain), Cloudflare credentials, TRAVEL_VISITS_PASSWORD source of truth
ci references to prd's MAPBOX_TOKEN and Cloudflare credentials — and nothing else the publish workflow

The two Mapbox tokens differ only in URL restriction. Building with the dev token ships a localhost-only token, and the live map then renders as a blank white globe while the token still validates — an easy failure to misdiagnose, and the reason publishing is automated.

The ci config exists so the workflow cannot read TRAVEL_VISITS_PASSWORD: the build never needs it (visits.enc ships already encrypted), and the ciphertext is public in this repo, so the password is the only thing protecting the trip log. GitHub Actions holds a Doppler service token scoped to ci — read-only, one config, revocable — as the repo secret DOPPLER_TOKEN. Because a service token resolves to exactly one config, doppler run needs no -p/-c flags, so there is no argument to get wrong.

To stage a build locally (for inspection — not for publishing):

doppler run -p travel-map -c ci -- node scripts/build_site.mjs

This copies web/dist/travel/ (so the map serves under /travel/), generates dist/travel/config.js from MAPBOX_TOKEN, content-hashes the local assets for cache-busting, and writes dist/_redirects pointing the site root at /travel/. It refuses to build if web/data/visits.enc is older than a locally-edited visits.source.json.

web/ stays a self-contained static bundle, so it can also be dropped onto any other static host as-is. (.env.example lists the same variables for anyone preferring a local .env with node --env-file=.env over Doppler.)

About

Interactive world map of visited cities, plotted as country-colored stars with Mapbox GL JS

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages