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.
This commit is contained in:
1 parent
16e06b2ffe
commit
4e0d2abbd0
13 files changed
+629
-2
No files matched your search
+51
-1
@@ -30,7 +30,11 @@ opened in a browser.
|
||||
`docs/third-party-notices.adoc`.
|
||||
|
||||
| `data/`
|
||||
| Local, gitignored data files, e.g. `offline.pmtiles`.
|
||||
| Local, gitignored data files, e.g. `offline.pmtiles` and uploaded
|
||||
GPX files under `data/gpx/`.
|
||||
|
||||
| `lib/maptool/`
|
||||
| Application code beyond routing: the GPX parser and file store.
|
||||
|
||||
| `docs/`
|
||||
| Project documentation, in AsciiDoc.
|
||||
@@ -110,6 +114,52 @@ This is a live, online-only overlay — it always needs internet access
|
||||
and is unrelated to the offline PMTiles basemap above. Per RainViewer's
|
||||
terms, the API is free for personal/non-commercial use.
|
||||
|
||||
== GPX tracks and waypoints
|
||||
|
||||
The "GPX" map control (`public/js/gpx.js`) opens a side panel to upload
|
||||
GPX files and toggle them on the map, individually.
|
||||
|
||||
Storage:: Uploaded files are stored as-is under `data/gpx/`, in
|
||||
whatever folder hierarchy the user types into the upload form (e.g.
|
||||
`Hiking/2026`) — `Maptool::GpxStore`
|
||||
(`lib/maptool/gpx_store.rb`) creates folders as needed. Every path
|
||||
coming from the client (upload folder, file path in a URL) is
|
||||
resolved with `File.expand_path` and checked against the store root
|
||||
before touching the filesystem, so `../..`-style path traversal can't
|
||||
escape `data/gpx/`.
|
||||
`GET /api/gpx/tree` returns the resulting folder/file tree as JSON,
|
||||
which the panel renders recursively (folders as `<details>`, files as
|
||||
a checkbox).
|
||||
|
||||
Rendering:: Checking a file fetches `GET
|
||||
/api/gpx/file/<path>` — `Maptool::Gpx` (`lib/maptool/gpx.rb`) parses
|
||||
the GPX with REXML and converts it to a GeoJSON `FeatureCollection`:
|
||||
`<wpt>` waypoints become `Point` features (with `name`, `desc` and
|
||||
`time` carried through as properties), each `<trk>` becomes one
|
||||
`MultiLineString` feature (one line per `<trkseg>`). MapLibre's
|
||||
`geometry-type` expression treats `MultiLineString` the same as
|
||||
`LineString`, so a single GeoJSON source per file with a `line` layer
|
||||
and a `circle` layer (each filtered by geometry type) is enough to
|
||||
render both waypoints and tracks. `<rte>` routes are not handled.
|
||||
|
||||
Like the radar overlay, GPX layers are removed by `map.setStyle()`
|
||||
(switching Online/Offline) and get re-added on `style.load` for
|
||||
whichever files were checked.
|
||||
|
||||
Waypoint popups:: Clicking a waypoint circle opens a MapLibre popup
|
||||
with its name, description and time. Click/hover handling is
|
||||
registered once per file (via `map.on(type, layerId, handler)`, a
|
||||
delegated listener that survives `setStyle()`) rather than on every
|
||||
`style.load` re-add, which would otherwise stack duplicate handlers
|
||||
and pop up several copies per click. Waypoint text comes straight from
|
||||
an uploaded file — untrusted input — so `public/js/gpx.js`'s
|
||||
`linkify()` turns bare URLs in the description into `<a>` links by
|
||||
splitting the text on a URL pattern and escaping every piece
|
||||
(including the URL itself, as both `href` and link text) before
|
||||
assembling any HTML; the URL pattern excludes quote characters, so a
|
||||
matched URL can never break out of the `href="..."` attribute. Every
|
||||
other value is HTML-escaped before being handed to `Popup#setHTML`.
|
||||
|
||||
== Networking
|
||||
|
||||
The Sinatra server only binds to `127.0.0.1`. It is not meant to be
|
||||
|
||||
Reference in new issue
Block a user