Static PWA: an OpenStreetMap with the Pedal Hidrográfico hydrography overlay, every passeio of the collective drawn on top of it, filterable by date, with the linked Instagram post embedded on click. Includes an in-browser GPX drawing tool and a photo overlay sourced from RDF/Turtle. Architecture and conventions in CLAUDE.md; ontology notes in docs/DESIGN.md; self-hosted backend in backend/README.md.
- OpenStreetMap base + custom hydrography overlay
(
https://telhas.pedalhidrografi.co/rmsampa-v2/{z}/{x}/{y}.png), togglable via the layer control. - Renders every passeio from
web/data/tours.ttlin white with a dark casing, labelled byData — Nome. - Numbered overlay per route (plain text + halo). Multi-series tours show
every series code stacked (e.g.
PH 79/BP 4). - Sidebar sorted by date descending. Date-window slider filters in real time.
- Click a route → modal embedding the linked Instagram post.
- Traçar GPX button: click the map to drop trackpoints, drag them to
move, Undo/Redo (also ⌘/Ctrl+Z / ⌘/Ctrl+Shift+Z), then Salvar GPX
downloads a
.gpxfile. Esc cancels. - Geo-tagged photo AND video overlay loaded from
web/data/uploads.ttl(both kinds live in the same RDF catalog). Click a marker → centered modal preview (popup near the marker when it fits; falls back to a bottom-sheet modal otherwise). Photo markers have a white border; video markers have a red-orange border and a small play glyph; both share the same density-based clustering layout. - Animação — toggles a marker spotlight pulse and a translucent ghost-video overlay on the map (clips with video tracks, segments picked at random with audio + image cross-fading). Each clip's GPS marker pulses with the audio's RMS amplitude; bracketed by a 1-second green intro and orange outro to flag the handoff between clips. Audio-only clips participate only in the audio loop, not the ghost video.
- Loop de áudio — same clip catalog, ambient audio-only mode with longer crossfades. Independent of Animação.
- Localização ao vivo — opt-in, pseudonymous, ephemeral live-location
sharing (the 📍 icon on the "Pessoas ao vivo" layer row): broadcast your
moving position under a nickname for a chosen retention window, and others
see each rider as a live marker + trail that expires on a server TTL.
Backed by the backend's
/live-location+/live-locationsendpoints (in-memory, never persisted). A Capacitor native shell (capacitor/) wraps this same web app so the broadcast keeps running with the screen off — see capacitor/README.md. - Mobile-first UI: bottom-sheet dialogs (Camadas, Rotas, Enviar, Ajustes, Ajuda), dual-thumb date slider with native date pickers on the labels, dark theme with orange accent. PWA installable as "Amora".
It's a fully static site with an optional self-hosted backend for uploads. Two stages:
-
Build steps (Python, with credentials).
web/data/tours.ttl(the Tour catalog) is maintained through the app itself:web/upload_tour.htmlposts to the backend's Tour CRUD endpoints. (The oldbuild-tours.pyCSV pipeline was removed — the catalog now carries hand-written narratives and announcement images that a spreadsheet rebuild would wipe.)python scripts/build-routes.pyreadsweb/data/tours.ttl, fetches every referenced GPX from RideWithGPS, downsamples each track to ≤400 points, and writes everything intoweb/routes.json. Unique RWGPS IDs are fetched once even when multiple tours share a route (e.g. anniversary re-rides). This is the only place RWGPS credentials are needed — the browser never sees them.python scripts/build-clips.py(optional) re-encodes any source videos inweb/clips/raw/to 360p + 720p mp4 +.m4aaudio + thumbnail jpg, and upserts each as aph:Videoinweb/data/uploads.ttl(withph:capturedDuringset to the closest tour within ±12 h). Requiresffmpegandexiftoolin PATH.
-
Runtime (browser only).
web/index.htmlloadsroutes.jsonand resolves data graphs viaweb/data/data_graphs.ttl. No backend required at runtime; serveweb/from any static host. For uploads and photo storage, run the backend — see backend/README.md.
pip install python-dotenv rdflib pyshacl # build + backend deps
cp .env.example .env
# fill in RWGPS_API_KEY and RWGPS_AUTH_TOKEN — see "Credentials" below
python scripts/build-routes.py # writes web/routes.jsonThen serve web/ with anything:
cd web && python -m http.server 8000
# open http://localhost:8000/Python 3.10+ recommended (uses str | None annotations and urllib).
Re-run build-routes.py to refresh routes.json after tour edits
(the backend also upserts routes incrementally on every /upload-tour).
After the build steps above, web/ is fully self-contained. Push it to
GitHub Pages, Netlify, S3, etc. and you're done — no server needed.
Photos won't show on a static mirror unless you also publish web/data/
and web/photos/ from a backend run.
.env is gitignored. Build steps are the only thing that touches
credentials.
RWGPS_API_KEY— request one at https://ridewithgps.com/api.RWGPS_AUTH_TOKEN— auth token for the user account that owns the routes. You can get it byPOSTing email+password tohttps://ridewithgps.com/users/current.json(response includesuser.auth_token), or from your account settings page.RWGPS_COLLECTION_PRIVACY_CODE— set if some routes are unlisted and need the collection privacy code.
- Instagram embeds only work for public posts. Private-account posts won't render in the iframe — the modal falls back to a "View on Instagram ↗" link.
web/routes.jsonis gitignored so credentials-derived data isn't committed by accident. To deploy via GitHub Pages without a build server, remove it from.gitignoreand commit the JSON.- The Traçar GPX tool produces a bare track (no elevation, no per-point timestamps — there's no real source for those when drawing from scratch). Most platforms (RWGPS, Strava, Komoot) will fill in elevation server-side on upload.
amora is free software under the GNU Affero General Public License v3.0
(AGPL-3.0) — see LICENSE. It was GPL-2.0 until September 2026;
the switch lets it bundle GPL-3.0 code (the leaflet-rotate map-rotation plugin)
and makes the network clause explicit: if you run a modified amora for other
people over a network, you must offer them its source.
Bundled third-party code keeps its own license: Leaflet and flatgeobuf (BSD-2-Clause), leaflet-rotate (GPL-3.0), N3.js, exifr, qrcode.js and leaflet.locatecontrol and geotiff.js (MIT), Tom Select (Apache-2.0), mediabunny (MPL-2.0).