Mapping memories: An interactive travelogue handcrafted with Leaflet.js, Python, and open-source geospatial resources.
⚠️ This project is under active development.
- Overview
- Gallery
- Features & Interactions
- How It Works
- Tools & Technologies
- Usage
- Data Documentation
- References
- Licenses
- Acknowledgements
- Author
Waypoints: A Personal BioGeography is a map-based storytelling project—an interactive memoir built from travels, memories, and photos. The map spans five continents (for now!): India's temples and backwaters, Central America's volcanoes and reef systems, Namibia's desert canyons, Peru's highland rail lines, Scotland's coastline, Bali's volcanic peaks, and dozens of places in between. Each waypoint opens a photo carousel; overlaid routes trace how those places connect by plane, train, car, boat, and foot.
What began as a simple way to pin photos on a map evolved into a data project: a Python pipeline reads travel records from Google Sheets, geocodes locations, generates route geometries, and produces the static files that power the map.
The current build includes:
- 47 major waypoints with photo carousels and personal descriptions
- 327 routes — 197 road trips, 47 flights, 34 boat journeys, 26 hikes, 23 train trips
- 35 activities (hike, snorkel, safari, kayak, and more)
- 6 landmark locations (future expansion layer)
The atlas overview uses color-coded markers to distinguish life imprints. Interactive popups connect individual locations to their associated photographs and narratives.
A photo-and-commentary popup viewed alongside the route layer, linking a specific travel experience to its geographic context.
An activity popup appears over a map populated with waypoints, activity markers, and travel routes. The view brings together location-specific details and the atlas's interactive controls, including route selection, map legends, zoom, About, and reset functions.
A location-level view combining a photo reel with written commentary, integrating personal media into the geographic browsing experience.
Activity markers and route lines illustrate the atlas's use at a finer geographic scale, from individual outings to broader travel patterns.
Activity markers and travel routes appear together, showing how the interface handles point features and connected paths at the regional scale.
Map layers (all toggleable)
- Waypoints — circle markers scaled by a 1–10 importance score; home residences and academic locations styled distinctly
- Activities — point markers with typed icons: hiking, snorkeling, kayaking, safari, paragliding, and more
- Routes — polylines for five transport modes, each with its own color and dash pattern, individually toggleable (plane, train, auto, boat, hike)
Basemaps — six options: Satellite (ESRI), Street Map (OpenStreetMap), National Geographic, Physical, Oceans, Grayscale
Popups & photo carousels — each marker opens a popup with photos, descriptions, and an optional zoom-to-area button; carousels support autoplay, fullscreen, and pinch/scroll zoom
Locations layer — landmark and place-type markers that appear automatically above zoom level 6
Welcome screen & guided tour — a splash modal on load followed by an optional Shepherd.js tour walking through the map's layers and controls
Legends — a main legend for waypoint types; a route-type legend (with line-style previews) appears when the Routes overlay is active
Travel data lives in Google Sheets; a Python script converts it to static files the map reads.
Google Sheets (source of truth)
│
▼
scripts/extract_data.py
│ assigns UUIDs · geocodes coordinates · generates route GeoJSON
▼
docs/resources/data/ ← overview.json, activity.json, location.json, routes.csv
docs/resources/geojson/ ← one .geojson per route (331 files)
│
▼
Leaflet.js map (index.html)
└── fetches JSON/CSV at load · loads GeoJSON per route · renders layers
Python pipeline
scripts/extract_data.py processes four sheets in sequence:
- UUID assignment — new rows get a stable UUID so records survive spreadsheet reordering
- Geocoding — missing coordinates are resolved via Nominatim; results are cached locally to avoid redundant API calls; ambiguous place names can be overridden in
scripts/manual_locations.py - Route geometry generation — by transport mode:
- Plane: geodesic great-circle arcs via GeographicLib, with correct handling of the International Date Line
- Auto: driving routes from the OpenRouteService API
- Train: real rail geometry sourced from Overpass Turbo (or a driving-route fallback if no GeoJSON is provided)
- Boat / Hike: manually sourced from Overpass Turbo or hand-drawn with geojson.io
- Write-back — enriched data (UUIDs, geocoded coordinates) is uploaded back to Google Sheets by default
Frontend
All vendor libraries are bundled locally — no CDN calls at runtime. The Leaflet map:
- Triples each marker at ±360° longitude to handle International Date Line crossings without gaps
- Uses custom Leaflet panes to control z-order precisely across routes, locations, waypoints, activities, and popups
- Parses
routes.csvwith PapaParse and loads each route's GeoJSON on demand - Draws ESRI basemaps (Satellite, Nat Geo, Physical, Oceans, Grayscale) via Esri Leaflet
| Layer | Tools |
|---|---|
| Frontend | Leaflet.js, Esri Leaflet, Shepherd.js, PapaParse, Panzoom, HTML/CSS/JS |
| Icons | Font Awesome, Material Design Icons |
| Data pipeline | Python, pandas, gspread, geopy (Nominatim), GeographicLib, openrouteservice |
| Geospatial sources | OpenRouteService API, Overpass API, Nominatim, geojson.io |
| Data formats | GeoJSON, JSON, CSV |
| Hosting | GitHub Pages |
Exploring the map
The project is live at johbry17.github.io/Waypoints. No local setup required.
- Click through the welcome splash to open the map; take or skip the guided tour
- Click any marker to open its popup and photo carousel
- Use the layer control (top-right) to toggle Waypoints, Activities, and Routes overlays
- Within Routes, toggle individual transport modes (plane, train, auto, boat, hike)
- Switch basemaps from the same layer control
Updating the data
Travel data is managed in Google Sheets. To add records and regenerate map files:
- Edit the relevant tab (Overview, Activity, Location, or Routes) in the spreadsheet
- For any new
autoortrainroute whose start/end location geocodes incorrectly, add manual coordinates toscripts/manual_locations.py - From the project root:
python scripts/extract_data.py
- Review generated files in
docs/resources/, then commit
See scripts/README.md for CLI options (--no-upload, --sheet-key, --verbose), configuration details, and the full field reference.
Configuration:
scripts/api_keys.jsonmust contain a Google service-account key (and optionally an OpenRouteService key). This file is excluded from version control.
data_dictionary.md— table schemas and field definitions for all four data tables (Overview, Activity, Location, Routes)scripts/README.md— pipeline walkthrough, CLI reference, geocode cache details, and per-transport-mode behavior
- Leaflet.js – For rendering interactive maps with custom markers and polylines
- Overpass Turbo – For querying OpenStreetMap data (trails, routes, POIs)
- Nominatim API – For geocoding and reverse geocoding
- OpenRouteService API – For automatically generating road trip routes
- GeographicLib – For geodesic calculations (e.g., great circle routes)
- geojson.io – For hand-drawing and editing GeoJSON routes
- GeoJSON Specification – Core format for encoding spatial data
- GitHub Pages – Hosting platform
- Google Sheets API and Python – For managing and updating project data
- Shepherd.js – For building the interactive guided tour
- Font Awesome and Material Design Icons – Iconography and styling assets
- Code: MIT License – see LICENSE
- Images: Personal content not for reuse – see LICENSE_IMAGES
If you're in a photo and want it removed, contact me: bryan.johns@informedwanderer.com
Thanks to everyone who's shared this beautiful world with me. And to the open-source community for the tools that made this project possible.
Bryan Johns
Last updated: October 2026
bryan.johns@informedwanderer.com | LinkedIn | GitHub | Portfolio
— Fluent in Data. Fluent in Human.