Black topbar banner with live map-center coordinates, a black header
(with NPS pictogram) on waypoint popups, offline basemap wood/park
colors unified with the online Liberty green, and the sidebar grouped
into the planned Explorer / Offline hub / Navigation menu sections.
ASSET_VERSION only proves *a* deploy happened recently, not *which*
commit -- not enough to answer "did my push actually deploy, or am I
looking at a stale browser cache" (today's actual answer: neither --
deploy was current, a GeoJSON cache predating a code change was just
never invalidated, since it only checks the source .gpx's mtime).
The CI deploy job now writes REVISION (short SHA) before dropping
.git; app.rb reads it at boot, falling back to "dev" locally/in CI's
own check job where the file never exists.
A .geojson file needs no GPX/FIT conversion — it's already the shape
gpx.js expects — so GpxStore#geojson now just reads and returns it
as-is, no cache sidecar. save/tree/the upload dropzone and its zip
handling all pick it up through the existing TRACK_EXTENSIONS list.
Also fixes a bug the new "cached" tree icon would have shipped with:
a GPX/FIT's own .geojson cache sidecar (e.g. "ride.gpx.geojson") also
matches the new .geojson branch in list_children, which would have
listed it as a second, phantom tree entry — excluded explicitly, with
a regression test.
A park4night/OsmAnd POI export (e.g. a p4n2gpx area export) tags every
waypoint with a category and a per-category color/icon in its <extensions>
block, all of which was previously thrown away — every waypoint rendered as
the same plain red dot, unreadable once a file has a few thousand of them.
Maptool::Gpx now captures category/color/icon (waypoint_style), matched by
namespace URI rather than local element name: a p4n2gpx file carries both
<osmand:icon> and <locus:icon> side by side, and REXML strips prefixes from
Element#name, so both report as just "icon" — matching on that alone was
silently picking the (useless) Locus drawable path instead of the OsmAnd
category key. Regression test added for this.
On the map: waypoints are colored by category (falling back to the plain
track color for anything uncategorized), a small hand-drawn icon set
(public/js/icons.js's POI_ICONS, rasterized to SDF sprites at runtime) draws
a glyph on top for the categories it covers, the points source is clustered
so a dense export collapses into count bubbles instead of a solid blob, and
a legend in the side panel lists whichever categories are currently visible.
Splitting each file's GeoJSON into a lines source and a points source
(rather than one mixed source with a geometry-type filter per layer) was
needed for clustering, since MapLibre's cluster support only understands a
source made entirely of points.
Track properties gain activity_type (FIT: sport enum from the
session/sport message, Garmin's own table; GPX: <trk><type> passed
through as-is) alongside the existing distance/elevation/duration.
Clicking a track line now opens a bottom panel with a chart per
metric the track actually has (elevation always; speed/cadence/heart
rate only when present — FIT's record fields or GPX's device
extensions). TrackStats.build_profile computes cumulative distance
the same way for both formats so they're directly comparable. One
pointermove listener drives a synchronized crosshair across every
chart, a single tooltip listing all series, and a map marker at the
hovered point's position — hovering the chart shows where on the
track that moment was.
Maptool::Fit reads Garmin's binary FIT format directly rather than
adding a gem — the one Ruby option depends on activesupport, unmaintained
since 2022, for a single-purpose binary parser. Only "record" messages
are read (position + altitude; everything else is skipped by byte
length, never interpreted); field numbers and scale/offset are
Garmin's own (fit-python-sdk's profile.py), and the framing plus all
extracted values were checked byte-for-byte against 5 real device
files from python-fitparse's test suite before this shipped.
A .zip upload (Garmin's bulk/FIT export format) is expanded into its
.gpx/.fit entries via rubyzip — that one's an actual dependency, since
ZIP's own format isn't the kind of narrow, single-purpose parsing FIT
above is.
A Gmail-watching Apps Script (documented in docs/deployment.adoc) posts
the session ID from each LiveTrack email to a new webhook, which stores
it in data/livetrack.json; a new sidebar panel lists what's been
received.
Upload/delete/move/create-folder now require logging in via Gitea
OAuth2 (only MAPTOOL_ALLOWED_GITEA_USER is granted write access);
reading stays public. Deploy secrets (session secret, OAuth2 client
credentials, allowed user) are written to .env from Gitea Actions
repo secrets on every deploy instead of living on the server by hand.
Also switch the container's bind address to "[::]" (dual-stack) so
the app is reachable over IPv6, not just IPv4.
Restructure the map controls from MapLibre corner buttons into a left
sidebar of icons with flyouts/panels (radar, GPX, GNSS), matching
OsmAnd's layout more closely:
- Base map and GPX selection are now full-height side panels docked
next to the sidebar (not small dropdown flyouts or a right-docked
panel), opening/closing exclusively via a small shared event bus
(panels.js) so only one is ever open at a time.
- The GPX panel's upload/new-folder controls are now icon buttons in
the header instead of an always-visible dropzone and form; dragging
a GPX file onto the tree, a specific folder, or straight onto the
map itself all upload it.
- New: Maptool::GpsdClient (lib/maptool/gpsd_client.rb) streams live
GNSS fixes from a local gpsd daemon over Server-Sent Events
(/api/gnss/stream) to a marker on the map (public/js/gnss.js) — one
background gpsd connection shared by every browser tab.
- The online map style (public/js/osmand-style.js) is now styled to
resemble OsmAnd's own rendering, applied to both the online and
offline (PMTiles) styles.
Also adds a GeolocateControl (browser geolocation) next to the zoom
controls.
Uploading is now drag & drop (or click to browse) instead of a form;
folders can be created any time, not just during upload; files can be
dragged onto a folder to move them, or dropped elsewhere in the tree
to move back to the top level; files and folders can be deleted. Both
drag sources (an OS file drag and dragging an existing tree entry)
land on the same drop targets, told apart by a custom drag payload
type versus the browser's own "Files" type.
Also sort folders before files in the tree (previously interleaved
alphabetically).
Upload GPX files into a folder hierarchy (traversal-safe path
resolution against the store root), browse them in a tree panel, and
toggle each one's waypoints/tracks on the map. Waypoints render as
clickable points showing name, description and time; bare URLs in the
description are turned into safe links. GPX layers survive switching
between the Online/Offline base map, same as the radar overlay.
Adds rexml as an explicit dependency — it moved to a bundled gem in
this Ruby version and isn't picked up by Bundler otherwise.