Skip to content

Repository files navigation

tree-walks

A small Python toolkit for turning a folder of geotagged photos into a JSON document describing a tree appreciation walk — an ordered sequence of points, each backed by a photo, ready to render in a map/gallery UI.

The pipeline

One folder per walk under walks/, carried through three stages. The source folder is the source of truth and is edited in place; publication produces a separate, throwaway artifact you can rebuild any time before deploy.

 downloaded photos          extract              edit                 publish
 (full EXIF, GPS)   ──►   tree-walks   ──►   tree-walks-editor  ──►  tree-walks-publish
                                                                          │
 walks/<name>/*.jpg  ─────────────────── edited in place ──────────►  publish/<name>/
                          + walk.json         (walk.json)              walk.json + media/
Stage Command Does
1. Extract tree-walks walks/<name> Read EXIF, order by capture time, write walk.json into the folder.
2. Edit tree-walks-editor walks/<name> Fix GPS, reorder, add titles/notes, pick the hero, shape the route.
3. Publish tree-walks-publish walks/<name> Write a clean, deployable publish/<name>/ (slim walk.json + web-sized media/).

Install & run

Uses uv for dependency management.

Every walk lives in its own folder under walks/ — drop a folder of photos in there, then point the tool at it:

uv run tree-walks walks/london-tree-appreciation-walk

📍 Photos work best with GPS location. Each stop is placed on the map from the photo's embedded GPS coordinates, so ideally shoot with location enabled — on most phones it's a per-app setting (iOS: Settings → Privacy & Security → Location Services → Camera; Android: Camera → Settings → Location tags). It doesn't have to be on all the time: geotagging every photo you take is a real privacy trade-off, so it's fine to switch it on just for a walk and off again afterwards. Photos without GPS aren't dropped — they're flagged location_missing: true and you place them by hand in the editor.

This scans the folder, reads each image's EXIF, lays the points out in capture-time order, and writes walk.json alongside the photos.

Re-running is safe for edits

walk.json is edited (by hand or via the editor UI) after it's generated — correcting bad GPS, adding titles/notes — so re-running merges rather than overwrites. Keyed by each point's stable id (the image filename stem):

  • a photo already in walk.json keeps its saved point untouched, including its position — your edits and any manual re-ordering win;
  • a new photo added to the folder becomes a fresh point from its EXIF, appended after the existing points;
  • a photo removed from the folder drops out of the walk.

Capture time only seeds the order: a fresh walk is laid out by EXIF timestamp, but the timestamp is never stored, so once a walk exists its saved point order is what sticks. Pass --rebuild to throw away the existing file and regenerate purely from EXIF (discards edits and re-orders by capture time).

Options

tree-walks <folder> [-o OUTPUT] [-n NAME] [--rebuild]

  folder          Folder of images to turn into a walk.
  -o, --output    Output path (default: <folder>/walk.json). Use '-' for stdout.
  -n, --name      Walk name (default: keep existing, else derive from folder name).
  --rebuild       Ignore the existing walk.json; rebuild from EXIF, discarding edits.

Editing a walk

Once a walk.json exists, open the behind-the-scenes editor to sanity-check the route and fix things before publication:

uv run tree-walks-editor walks/london-tree-appreciation-walk

This starts a local server (default http://127.0.0.1:8757) and opens a map + gallery editor in your browser. From there you can:

  • See the ordered route on a map, with numbered, draggable markers and the photo for each stop.
  • Reorder stops by dragging rows in the sidebar (the sequence numbers re-derive automatically).
  • Fix bad GPS by dragging a marker, or by typing new coordinates.
  • Repair missing locations (location_missing: true) — pick the point and click Set on map to drop it where it belongs.
  • Add titles and notes for each stop.
  • Choose the cover image — pick a stop and click Use as walk cover to mark its photo as the walk's hero image (hero_image), the lead image a published site shows at the threshold. Only one stop can be the cover.

Hit Save (or ⌘/Ctrl-S) to write back to walk.json; the previous version is kept alongside as walk.json.bak. The server re-sequences points, recomputes bounds/point_count, clears location_missing for any point you've located, and stamps an edited_at. Download exports the JSON without touching disk.

Options: --port, --host, --no-open. The editor pulls map tiles from OpenStreetMap, so it needs a network connection.

Publishing a walk

Once a walk is edited, turn it into a clean, deployable folder:

uv run tree-walks-publish walks/london-tree-appreciation-walk

This reads the source folder (never mutating it) and writes a separate, self-contained artifact — by default to publish/<walk>/:

publish/london-tree-appreciation-walk/
  walk.json          # slim public schema, relative media/ paths
  media/
    IMG_*.jpg        # web-sized (~1600px, q80), all metadata stripped

What it does:

  • Optimizes images — resizes each photo so its longest edge is ≤ 1600px and strips all metadata. This is not optional polish: your originals still carry GPS and camera tags, and a public site must not ship those. (It also turns ~7 MB originals into ~0.7 MB web images.)
  • Anonymises filenames — renames images to their walk sequence (01.jpg, 02.jpg, …). Phone filenames like IMG_20260705_192004.jpg encode the exact capture time in plaintext; next to each point's public location that would re-leak the space+time trace stripping EXIF is meant to prevent. The source folder keeps its original names — only this throwaway published copy is renamed.
  • Slims the JSON — keeps only what a viewer needs and rewrites image paths. Kept per-point: id, sequence, image, location (lat/lng), title, notes, route_waypoint (the map draws the route through it). The id and image are re-derived to the anonymised sequence stem (01, 01.jpg). Dropped: edited_at, route_vias, per-point location_missing, location.altitude, and the route's profile/signature/generated_at.
  • Gates on readiness — because publish is the last check before deploy, it reports what isn't ready: a point with no location is a hard error (it can't go on the map); empty titles/notes and a missing hero image are warnings (the site is only as good as those prompts).

Publishing into the site (--to)

To publish straight into the sibling Astro site (the tree-appreciation-walks repo, cloned alongside this one), point --to at its root instead of using -o:

uv run tree-walks-publish walks/london-tree-appreciation-walk --to ../tree-appreciation-walks

Astro wants a walk split across two locations, so this writes:

../tree-appreciation-walks/
  public/walks/london-tree-appreciation-walk/IMG_*.jpg   # served at /walks/<id>/…
  src/content/walks/london-tree-appreciation-walk.json    # the content-collection entry

In this layout each point's image is a bare filename (the site resolves it against /walks/<id>/), while hero_image is a full path (/walks/<id>/<file>) so it's usable on its own — index cards, the page hero, og:image. Only this walk's image folder and JSON file are replaced on re-publish; sibling walks are left untouched. The published schema must match tree-appreciation-walks/src/content.config.ts — keep the two in sync.

Options

tree-walks-publish <folder> [-o OUTPUT | --to SITE] [--no-optimize]
                            [--max-edge PX] [--quality Q] [--allow-incomplete]

  -o, --output        Self-contained output dir (default: publish/<folder-name>).
  --to SITE           Publish into an Astro site root instead of -o.
  --no-optimize       Copy originals as-is (WARNING: keeps EXIF, incl. GPS).
  --max-edge          Longest edge in px for resized images (default: 1600).
  --quality           JPEG quality 1–95 (default: 80).
  --allow-incomplete  Publish even if some points have no location.

Output is fully regenerated each run (the previous copy of that walk is replaced), so re-run it freely as you finish editing. The self-contained publish/ folder is git-ignored — treat it as a build artifact, not source.

What it reads

Per image, via EXIF (Pillow) — only what's needed to map the route:

  • Capture time (DateTimeOriginal) — used to order the walk.
  • GPS latitude / longitude / altitude — decoded from EXIF DMS to decimal degrees.

Identifying tags (camera make/model, etc.) are intentionally not read, to prefer anonymity. Photos without a timestamp are appended at the end (in filename order) rather than dropped. Photos without GPS are kept and flagged with location_missing: true so the editing UI can prompt for a corrected location.

Output schema (walk.json)

This is the source/working walk.json produced by extract and edited by the editor. Publishing emits a slimmer variant (see Publishing a walk) with the editor-only fields removed and image paths rewritten for the target.

{
  "id": "london-tree-appreciation-walk",   // folder name
  "name": "London Tree Appreciation Walk",
  "schema_version": 1,
  "created_at": "2026-07-06T...Z",         // UTC ISO-8601, first generation
  "point_count": 16,
  "bounds": {                              // null if no photo has GPS
    "min_lat": 51.573, "min_lng": -0.154,
    "max_lat": 51.584, "max_lng": -0.123
  },
  "points": [
    {
      "id": "IMG_20260705_165608",         // image stem, stable id
      "sequence": 1,                        // 1-based walk order
      "image": "IMG_20260705_165608.jpg",  // filename, relative to folder
      "location": { "lat": 51.583, "lng": -0.131, "altitude": 0.0 },
      "title": "",                          // editable — for the UI
      "notes": "",                          // editable — for the UI
      "location_missing": false,            // true when GPS was absent
      "route_waypoint": true                // false = skip this stop on the route
    }
  ],
  "hero_image": null,                       // cover photo filename, or null (set in editor)
  "route": null,                            // editor-saved foot-walking route, or null
  "route_vias": [],                         // [lng, lat] shaping points for the route
  "google_maps_url": null,                  // walking-directions link through the waypoints
  "edited_at": "2026-07-06T...Z"            // added by the editor on save
}

title, notes, location, route_waypoint, and hero_image are the fields the editor lets you set (several photos have bad GPS fixes). route, route_vias, google_maps_url, and edited_at are written by the editor and carried across re-runs, so hand-shaped routing is never silently dropped. schema_version lets the format evolve safely.

Project layout

walks/                              # source walks live here, one folder each
  london-tree-appreciation-walk/     # (contents git-ignored: your originals carry GPS/EXIF)
    IMG_*.jpg                        # originals, full EXIF
    walk.json                        # working format (generated + edited)

publish/                            # generated, deployable (git-ignored)
  london-tree-appreciation-walk/
    walk.json                        # slim public schema
    media/IMG_*.jpg                  # web-sized, metadata-stripped

src/tree_walks/
  exif.py      # read EXIF: timestamp + GPS (DMS->decimal) only
  walk.py      # data models + build_walk(): ordering, bounds, schema
  cli.py       # argparse entrypoint (tree-walks)
  editor.py    # local map editor server (tree-walks-editor)
  editor.html  # the editor's single-page UI (Leaflet map + gallery)
  publish.py   # build the deployable artifact (tree-walks-publish)

About

A back office editorial tool for curating a tree appreciation walk dataset from a set of Geotagged images

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages