Commit Graph
25 Commits
Author SHA1 Message Date
randy 12635a48c3 Badge the exception (not yet cached) instead of the near-universal case
GpxStore#save converts eagerly, so "cached" is true for nearly every
file in the tree already — badging that was just noise. Flipped to
PENDING_ICON, shown only for the rare case: a .gpx/.fit dropped
straight into data/gpx/ on the filesystem, bypassing the app, and not
yet opened.

Also add an explicit download button next to delete (DOWNLOAD_ICON) —
the file name itself has been a download link since the last commit,
but isn't obviously clickable at a glance.
2026-09-07 22:59:02 +02:00
randy cd92dadf6a Let a cached file's GeoJSON be downloaded by clicking its tree name
Once a .gpx/.fit is cached, the tree shows it under its converted name
("ride.gpx" -> "ride.gpx.geojson", matching the real cache file next
to it on disk) instead of still reading as un-converted -- the
GeoJSON is what's actually on the map from that point on, and the
original is never deleted, just not re-shown as itself. The name is
now a real link (GET /api/gpx/file/<path>?download=1) instead of
plain text, the only way to get that converted form out of the app.

Verified in a real browser that an <a> nested in a <label> claims its
own click rather than the label forwarding it to the checkbox, so
downloading and toggling visibility don't fight over the same click.
2026-09-07 22:56:46 +02:00
randy dd0ad6d845 Show waypoint photos from newer p4n2gpx exports
A newer p4n2gpx run (Ruby rewrite, ~/test/mapping/p4n2gpx) started
emitting one plain GPX <link href> per park4night listing photo,
which this app parsed but never displayed. Gpx.waypoint_photos
collects them into a photos property; the waypoint popup renders them
as a row of thumbnails above the description, each opening full-size
in a new tab. Absent on older exports and plain GPS waypoints, so the
row just doesn't render for those.
2026-09-07 22:38:22 +02:00
randy 96065b1237 Home button fits all of Slovakia; add an overview map, top-right
CI / check (push) Successful in 1m33s
CI / deploy (push) Successful in 27s
Home now fitBounds()s a country-wide bbox instead of resetting to
CENTER/ZOOM's Bratislava point, since Home resetting into the same
city you'd already be looking at wasn't useful.

The overview map is NPS's own overview plugin (npmap5-plugins, still
on an unmerged branch), vendored and converted from its UMD dist
build to a plain ES module. Patched one MapLibre API break found while
verifying it: the plugin reads map.transform (public in the MapLibre
~2.4 it targets) to relax the inset's polar pan limit; MapLibre 6 (this
app's vendored version) no longer exposes .transform at all, so it
threw on load. Guarded to skip that cosmetic adjustment when absent
instead.
2026-09-07 22:30:56 +02:00
randy b6a9c1546e Add a distance/area measuring tool
CI / check (push) Successful in 46s
CI / deploy (push) Successful in 15s
A ruler button in the top-left control stack, matching NPS's own
Area/Distance ruler. Click to add vertices; distance sums haversine
segment lengths, area uses the spherical-excess polygon formula
(same one turf.js's area() uses) rather than a flat shoelace, since a
flat approximation drifts at park-boundary scale. SI units only
(m/km, m²/km²) — no imperial toggle.
2026-09-07 22:15:08 +02:00
randy 16d36e4441 Add a zoom-level readout to the map control stack
CI / check (push) Successful in 49s
CI / deploy (push) Successful in 18s
Matches the small numbered square NPS keeps next to its own zoom
buttons — a passive display, not a button, so it's plain markup with
no click behavior.
2026-09-07 22:12:06 +02:00
randy 8fa3391c6b Add an Alt+click debug inspector, NPMap4-infobox style
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 13s
NPMap4 has infoboxControl: a generic "click anything, show every raw
property" debug view, kept separate from a dataset's own curated popup.
Not portable code (Leaflet), but the concept is: Alt+click anywhere now
opens a popup listing every rendered feature at that point via
map.queryRenderedFeatures — base map OSM tags included, not just this
app's own GPX/FIT/radar layers — with a raw key/value table per feature.

No dedicated UI for it on purpose: a keyboard-modified click needs no
button, toggle state, or cursor change of its own, and it's fine for it to
coexist with a layer's own click handler firing on the same click (e.g. a
GPX waypoint's normal popup) since this is a power-user/debugging
affordance, not a replacement for those. Feature properties can come from
an uploaded GPX/FIT file (untrusted input) via this app's own layers, so
every value is escaped the same way gpx.js's popups already are.
2026-09-07 20:34:15 +02:00
randy fd8852c0e1 Fix meta-row color regression, show Services/Activities as icon tags
CI / check (push) Successful in 43s
CI / deploy (push) Successful in 15s
The gray color from the previous commit landed on .gpx-popup__meta (the
whole row, label and value both) instead of just .gpx-popup__meta-label —
only the label was supposed to be muted, the value should read as normal
text same as the description above it.

While in there: Services/Activities were showing park4night's own raw,
comma-separated codes ("animaux, point_eau, wc_public"), which don't mean
anything to a reader on their own. PARK4NIGHT_TAGS (icons.js) maps the
codes p4n2gpx's exports actually use to a human label and, where the
National Park Service's icon set (already used for POI_ICONS) has a
reasonable match, a small icon — formatTags (gpx.js) renders each as an
icon+label tag instead of raw text. A code not in that table just falls
back to its own raw text, so nothing is ever hidden.
2026-09-07 17:25:37 +02:00
randy 4447b6cebb Make Rating/Services/Activities in waypoint popups muted, not bold
CI / check (push) Successful in 43s
CI / deploy (push) Successful in 13s
Secondary metadata, not something worth visually competing with the actual
description — dropped the bold label weight and colored the whole meta
block the same muted gray already used for category/time elsewhere in the
popup.
2026-09-07 17:07:25 +02:00
randy 3759f7a5c9 Format the structured lines in park4night waypoint popups
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 16s
A park4night <desc> packs a free-text narrative and several lines of
structured data (address, rating, services, activities, the listing URL)
into one field, separated only by newlines. Rendered as a single paragraph
it read as a wall of run-together text — no visual hierarchy between the
actual description and what's really key-value metadata.

formatDescription now recognizes "Label: value" lines (Rating/Services/
Activities) and bare-URL lines, and gives each its own row instead of
burying them in the narrative paragraph: a bordered meta block with bold
labels, and an isolated link line. Anything that doesn't match — the real
narrative text, or a plain GPX file's <desc> with no such structure — stays
exactly as it rendered before, so this only adds structure, never removes
or reorders text.
2026-09-07 17:03:36 +02:00
randy 0b18dc018a Color-code, cluster and add icons to POI waypoints
CI / check (push) Successful in 47s
CI / deploy (push) Successful in 16s
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.
2026-09-07 16:18:06 +02:00
randy a0476f1204 Add a Distance/Time toggle to the profile chart's x-axis
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 16s
TrackStats.build_profile now also computes elapsed_secs per point
(relative to the first known timestamp), alongside the existing
cumulative distance. Point x-positions stay distance-based either
way — only the shared axis strip's 5 tick labels (and the tooltip)
switch between the two, so toggling is a cheap re-render rather than
rebuilding the charts. The toggle only appears when a track actually
has elapsed-time data to switch to.
2026-09-07 13:54:49 +02:00
randy 4f7fe3c2a4 Show min/max y-axis values on each profile chart
CI / check (push) Successful in 40s
CI / deploy (push) Successful in 15s
Plain HTML overlays, not SVG text: the chart's viewBox scales width
and height independently (preserveAspectRatio="none", for a
responsive width at a fixed pixel height), so text drawn inside that
coordinate space would get stretched by the mismatched factor.
2026-09-07 13:44:21 +02:00
randy 105651f005 Add activity type and a click-to-open track profile panel
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 15s
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.
2026-09-07 13:38:49 +02:00
randy 6b8ea27e22 Show Live/Ended status on LiveTrack sessions
CI / check (push) Successful in 57s
CI / deploy (push) Successful in 26s
A session's own end timestamp is absent until the activity is
actually stopped, so its presence/absence is the status signal — no
new data needed, just reading what was already there. Shown as a
badge next to the session in the list and in its map hover popup.
2026-09-06 23:32:58 +02:00
randy 0ee98d8f07 Summarize LiveTrack sessions and prune stale ones
CI / check (push) Successful in 48s
CI / deploy (push) Successful in 23s
Fetching a session's track now also grabs its summary (name, activity
type, distance, duration) from the same page and stores it — sessions
the panel has actually shown replace their raw UUID with that summary,
and are kept indefinitely regardless of age. A session nobody ever
opened (no summary) is more likely test/dead data than something worth
keeping, so it's dropped automatically after 48h.
2026-09-06 22:18:25 +02:00
randy f4b5237f01 Render a session's track on the map
CI / check (push) Successful in 47s
CI / deploy (push) Successful in 19s
Clicking a LiveTrack session now fetches and draws its track (line +
latest position). Garmin's own API 403s any plain server-side request
(Cloudflare bot protection), so LiveTrackClient fetches the session
page instead and pulls the track out of the server-rendered data
embedded in it. The webhook now captures the session's token too,
since the id alone isn't enough to fetch anything.
2026-09-06 22:07:13 +02:00
randy f9ddf860ea Add Garmin LiveTrack session tracking
CI / check (push) Successful in 6m49s
CI / deploy (push) Successful in 23s
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.
2026-09-06 21:05:35 +02:00
randy e13d1a1e97 Require Gitea login to write GPX data; IPv6-capable bind
CI / check (push) Successful in 6m28s
CI / deploy (push) Successful in 13s
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.
2026-08-27 18:02:37 +02:00
randy 06ea8ea141 Add file-kind icons to the GPX tree, fix drop into an empty panel
Each tree entry now shows a small waypoint/track icon based on a
cheap substring check for <wpt>/<trk> (Maptool::Gpx.kinds) rather than
a full REXML parse, since re-parsing a several-thousand-waypoint file
on every tree fetch would make the panel noticeably slower to open.

Also fix a real drop-target bug: .gpx-tree had no flex:1, so a
short/empty tree left blank panel space below it that wasn't part of
the drag & drop target — dropping a file there silently did nothing.
2026-08-25 22:38:41 +02:00
randy 87df90f75c Redesign UI as an OsmAnd-style sidebar, add live GNSS position via gpsd
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.
2026-08-25 19:39:13 +02:00
randy d813dd0b20 Add GPX drag & drop, folder management, delete, and folder-first sort
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).
2026-08-25 15:14:09 +02:00
randy 4e0d2abbd0 Add GPX upload with hierarchical folders and map display
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.
2026-08-25 14:05:39 +02:00
randy 5afb130c45 Switch to vector maps (MapLibre GL JS + PMTiles) and add RainViewer radar
Replace Leaflet/raster tiles with MapLibre GL JS: an online OpenFreeMap
style and an offline style built from a local PMTiles archive via
pmtiles.js and protomaps-themes-base (no glyphs/labels offline by
design, see docs/architecture.adoc). Frontend libraries are vendored
locally rather than loaded from a CDN so the app keeps working with no
internet access.

Also add an animated RainViewer precipitation radar overlay (toggle,
play/pause, scrub slider), matching the default time window of the
weather-radar-card Home Assistant integration.
2026-08-25 01:18:36 +02:00
randy 7329497e95 Initial scaffold: Sinatra map app with Tauri desktop shell
Sinatra backend serving a basic Leaflet map, wrapped in a Tauri
desktop shell that starts the same server in a native window.
2026-08-24 22:57:03 +02:00