Commit Graph
28 Commits
Author SHA1 Message Date
randy 707e71caf1 Add elevation-tinted terrain (hypsometric color-relief layer)
Reuses the same raster-dem source the hillshade toggle already added
— a color-relief layer tints by elevation band (green valleys through
brown mid-slopes to pale grey peaks, in this app's own palette) under
the hillshade layer, so one toggle gives the combined shaded-relief
look instead of two separate layers.
2026-09-09 09:33:00 +02:00
randy 77a4c96b1c Add a hillshade/terrain-shading toggle to the map
Sidebar toggle (new HillshadeControl) adds a raster-dem hillshade
layer: public Terrarium tiles when online, or a local
data/terrain.pmtiles when present (mirrors offline.pmtiles' optional-
local-file pattern, new /api/terrain-tiles + /tiles/terrain.pmtiles
endpoints). No offline terrain data is bundled, so the toggle is
online-only until one is generated — see docs/development.adoc for
how.
2026-09-09 09:27:17 +02:00
randy f49e44800a Distinguish unpaved tracks from paved roads on both basemaps
Offline: fill in protomaps' "other" road-kind color (unset before,
so tracks/paths silently fell back to plain white against the cream
background); also fixes a dead "minor" theme key that was never read
by the actual paint expression (only minor_a/minor_b is), so ordinary
streets were quietly using the base theme's default yellow instead of
this app's cream.

Online: Liberty's own road_service_track layer merges paved service
roads with unpaved tracks into one color, which isn't safe to just
recolor brown (real driveways aren't unpaved). Clones it into a
second layer scoped to class=="track" only, dashed and dirt-brown,
matching the offline treatment without mislabeling paved service
roads.
2026-09-09 09:17:00 +02:00
randy 8ca5ca9bc1 Add an offline-data status dot to the topbar
Small green/gray indicator reusing the existing /api/offline-tiles
check, showing at a glance whether this server has offline map data.
Closes out the top banner TODO item — DEM elevation and a hamburger
menu are intentionally left out (no DEM layer yet, and the sidebar
isn't mobile-collapsed).
2026-09-09 09:11:52 +02:00
randy bc9ea58614 Start the NPS Unigrid visual redesign
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.
2026-09-09 09:02:29 +02:00
randy 6e5676c890 Make "find my location" actually follow GNSS, gated on the sidebar toggle
CI / check (push) Successful in 44s
CI / deploy (push) Successful in 18s
Two fixes based on feedback after the last commit:

1. The map wasn't following the position at all -- only the first fix
   recentered, later updates just moved the marker. Now every fix
   recenters (easeTo, not flyTo, after the first one, so it doesn't
   repeat that zoom swoop on every update) -- matches how
   GeolocateControl's own trackUserLocation always behaved.

2. GNSS should only be used when explicitly turned on via the sidebar
   "GNSS" icon, not whenever a receiver happens to have a fix --
   simpler to test against the simulator that way, and puts the
   visitor in control of whether GNSS is engaged at all. GnssControl
   now owns following the map on its own (same flyTo-once/easeTo-after
   pattern) whenever it's enabled; LocateControl checks
   GnssControl#isEnabled() and, when true, just jumps to
   #getPosition()'s current fix and reuses its existing .gnss-marker
   instead of polling/marking on its own. Only falls back to the
   browser's own geolocation when GNSS is off.

Verified end to end with the GNSS simulator: enabling sidebar GNSS
recenters immediately and keeps following over several polls;
clicking Locate while GNSS is on jumps to the same position without
creating a second marker or touching its own active state; turning
GNSS back off and clicking Locate correctly falls through to the
browser geolocation path instead.
2026-09-08 00:56:25 +02:00
randy aec665179f Prefer this app's own GNSS receiver for "find my location"
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 22s
LocateControl (public/js/locate.js) replaces MapLibre's own
GeolocateControl -- same button, reusing its exact CSS/icon so it's a
visual drop-in, but sourced differently. Checks GET /api/gnss/position
once on click: if a fix is available (gpsd, possibly
bin/gnss_simulator.rb's simulated one), tracks that instead of the
browser's separate (and often less accurate) location service, which
is now only a fallback. Only the first fix recenters the map; later
updates just move the marker, unlike GeolocateControl's own default
of recentering on every update, which would fight a pan/zoom made in
the meantime.

Verified end to end with the GNSS simulator running: clicking the
button centered on the simulated position and dropped a marker,
clicking again cleared both, no console errors.
2026-09-08 00:46:14 +02:00
randy ceaaa0bb92 Apply the NPS-inspired palette to the online map too
ONLINE_STYLE_PATCH mirrors OFFLINE_THEME's color choices onto
OpenFreeMap Liberty's OpenMapTiles layer ids (a separate hand-built
table -- the two schemas share no layer names). loadOnlineStyle
fetches Liberty's style.json once, patches it in place, and the
result replaces the plain URL everywhere ONLINE_STYLE is used --
MapLibre accepts a style object anywhere it accepts a style URL, so
applyStyle needed no change.

Runs before the map (and the rest of the page) is constructed, so
there's no flash of Liberty's own colors first -- but with a 3s
timeout and a catch-all fallback to the plain URL, since this app is
meant to keep working offline in the field and blocking startup on a
remote style server would undermine that.

Verified in a real browser: online map now visually matches the
offline theme (cream ground, brown/khaki land cover, green parks,
muted roads), no console errors, Online/Offline toggle still works.
2026-09-07 23:12:20 +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 88637ded22 Move the locate-me button into the top-left control stack
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 16s
GeolocateControl was alone at bottom-right while Home/zoom/base-map were
grouped top-left — split across two corners instead of one stack like
the National Park Service's own map controls. Moved it in, between zoom
and the base-map button.
2026-09-07 20:37:43 +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 215957382a Group Home/zoom/base-map into one top-left stack, NPS-style
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 16s
Home was alone at top-left and zoom (NavigationControl) was down at
bottom-right with GeolocateControl — split across two corners instead of
grouped together like the National Park Service's own map controls
(https://www.nps.gov/, screenshot: Home, zoom in/out, base map, all
stacked in one place).

Moved NavigationControl up to join Home (showCompass: false — this map
never rotates, so a bearing-reset button would be dead weight), and added
MapLayersButton, a small map-corner control that's a second entry point
into the same Online/Offline panel LayerToggleControl already manages in
the page's sidebar (both go through the same toggle() now, so there's one
source of truth for whether the panel's open). GeolocateControl stays
alone at bottom-right — it's a different action (go to my actual location)
from Home (reset to the app's default view), which was worth clarifying
since the two are easy to conflate.
2026-09-07 20:23:37 +02:00
randy be6d61e136 Stop coloring forests/scrub the same green as parks
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 16s
The previous pass applied Park Tiles' real park/protected-area green
(#b3e378) to wood_a/wood_b/scrub_a/scrub_b as well as park_a/park_b —
but in Park Tiles' own Imagery style, that green is specifically the
protected-area/administrative color; actual vegetation there is just the
aerial photo underneath, which this offline style has no fill equivalent
of. Applying it everywhere made the whole map read as green instead of
"brown with green parks popping out", which is the actual character of
their palette. Forests/scrub now render in muted brown/khaki instead,
leaving the vivid green for park_a/park_b alone.
2026-09-07 20:17:09 +02:00
randy a95964db77 Recolor the offline theme towards NPS Park Tiles' palette
CI / check (push) Successful in 44s
CI / deploy (push) Successful in 13s
Extended OFFLINE_THEME's color-role overrides to lean into the warm,
Harpers-Ferry-Center-derived look of the National Park Service's own "Park
Tiles" cartography, rather than the cooler default protomaps-themes-base
palette. A handful of values (background, the park/protected-area green, the
two road-importance tiers, the park-boundary line color) are the genuine
paint colors pulled from a public Park Tiles style JSON — the "Imagery"
variant is the only one of NPS's four styles served from a plain,
unauthenticated URL; "Standard"/"Slate"/"Light" resolve through Mapbox
Studio's private API with an access token, which isn't something to
piggyback on even though it's technically visible in their own JS bundle.
The rest of the palette (forest/scrub greens, water, sand, buildings) is
reasoned to match that same family rather than sourced.

Recreating the color choices is fine — that's not the kind of thing a data
license restricts. What stays off-limits is their actual tile service: its
OSM/terrain layers are licensed through Mapbox Atlas, a commercial,
non-sublicensable data service scoped to NPS's own use.
2026-09-07 20:00:50 +02:00
randy 170af80cac Add a Home button and fill icon gaps from NPS's newer symbol library
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 18s
Two things pulled in from github.com/nationalparkservice/npmap5-plugins:

- A "Home" button (public/vendor/npmap-home-button/, NPS's own MapLibre
  control) pinned to the app's CENTER/ZOOM, sitting top-left on the map. Now
  that the URL hash permalink can open the map at any view, "current view"
  and "the default view" aren't the same thing anymore, so a quick way back
  to the default is worth having.

- npmap5-plugins bundles a newer, actively-maintained version of the same
  NPS symbol-library already used for POI_ICONS (confirmed by its
  maintainer to be the designated replacement for the older standalone
  repo: nationalparkservice/npmap5-plugins#4), with a much larger icon
  vocabulary and a simpler CC0 (public domain) license. All 25 icons
  already in use came back byte-identical from the new source, so this
  just repoints their attribution — but it also has icons the old repo
  didn't: `forest` (POI_ICONS) is now the official "wilderness" pictogram
  instead of hand-drawn, and three of the five previously icon-less
  Services/Activities tags (wifi, mobile data coverage, winter/snow
  camping) now have real icons too. Only bakery and vehicle wash still
  have no NPS equivalent.
2026-09-07 19:45:19 +02:00
randy 481233c232 Hide the Base map switch when there's no offline map to switch to
CI / check (push) Successful in 46s
CI / deploy (push) Successful in 19s
The Online/Offline sidebar icon was always shown, even on an install with
no data/offline.pmtiles — offering a choice that can't actually work.
LayerToggleControl#remove() removes the icon (and hides its panel) once the
existing /api/offline-tiles check confirms there's nothing to switch to; a
transient failure of that same-origin check leaves the switch as is rather
than guessing, since that's the request itself failing, not evidence
offline tiles are unavailable.
2026-09-07 17:32:05 +02:00
randy 152a08f922 Sync the map view to the URL, like Google Maps/OSM permalinks
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 19s
Panning/zooming now updates #map=<zoom>/<lat>/<lon> in the URL, and loading
a URL with one already opens at that view instead of the default center —
so copying the URL bar shares the exact spot someone's looking at, and a
page reload doesn't lose the current view either.

This is MapLibre GL JS's own built-in Hash control (hash: "map" on the Map
constructor) — it already does exactly this, replaceState and all, no
custom code needed.
2026-09-07 17:12: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 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 0887ccbfe7 Drop the OsmAnd-recolor map theme, add offline labels and a lighter palette nudge
Remove osmand-style.js (the per-layer color-patching approach) and its
docs entirely — back to the plain OpenFreeMap "liberty" style online,
and protomaps-themes-base's stock theme offline as the base.

Offline maps previously had no text labels at all: the
protomaps-themes-base layers() call never passed a lang option, which
is required to include its label layers. Fixed, backed by vendored
Noto Sans Regular/Italic/Medium glyph PBFs (ranges 0-255 and 256-511
only — Basic Latin + Latin-1 Supplement + Latin Extended-A, enough for
accented Central European place names; other scripts won't render
offline). Verified against the actual offline.pmtiles vector layer
schema that the name/name:en/script fields this expects are present.

Also nudge a handful of OFFLINE_THEME color roles (background, water,
wood, buildings, the two road-importance tiers) towards the online
style's actual palette (sampled from its style JSON) so switching
Online/Offline doesn't look like two different apps — deliberately
partial, not the removed full recolor.
2026-08-25 22:38:56 +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 008e034932 Add geolocate control to the map
Lets the user center the map on their current location, with an
accuracy circle and continuous tracking.
2026-08-25 15:18:55 +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