Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
750f6c8fb0 | ||
|
|
bf17e00463 | ||
|
|
058c0990b4 |
No files matched your search
@@ -16,6 +16,13 @@ module Maptool
|
||||
set :public_folder, File.join(__dir__, "public")
|
||||
set :views, File.join(__dir__, "views")
|
||||
|
||||
# sinatra/json defaults to MultiJson, whose `encode` (what
|
||||
# sinatra-contrib 4.2.1 actually calls) is deprecated in newer
|
||||
# multi_json versions in favor of a constant this app has no other use
|
||||
# for — stdlib JSON already does the same job and every use of the
|
||||
# `json` helper below is a plain hash/array, nothing MultiJson-specific.
|
||||
set :json_encoder, JSON
|
||||
|
||||
# A persistent secret across restarts/redeploys — otherwise every deploy
|
||||
# would invalidate every logged-in session's cookie (see
|
||||
# docs/deployment.adoc). Falls back to a random one so the app still
|
||||
|
||||
@@ -691,6 +691,15 @@ require login, guarded by `require_login!` in `app.rb` — and so does
|
||||
exporting a file (`GET /api/gpx/file/*?download=...`), even though
|
||||
plain viewing of that same URL (no `?download`) doesn't; not a write
|
||||
exactly, but treated the same as the others rather than left open.
|
||||
The tree UI (`public/js/gpx.js`) mirrors this: every control for a
|
||||
gated action (delete, download, upload, new folder) is left out of
|
||||
the DOM entirely when logged out rather than shown and failing on
|
||||
click — `isLoggedIn()` checks for the topbar's own "Log out" link
|
||||
(`views/index.erb` only renders one when `logged_in`) instead of
|
||||
threading a separate flag through, since that's already there and
|
||||
login state can't change without a full page reload anyway. Delete
|
||||
exists on both file and folder rows — both are gated, easy to miss
|
||||
one and only catch it by testing an actually-logged-out session.
|
||||
|
||||
Login delegates to the Gitea instance at `git.priesol.net` via OAuth2
|
||||
(`GET /login` redirects to Gitea's authorize endpoint, `GET
|
||||
|
||||
@@ -246,6 +246,19 @@ own work (a map viewer, not a publisher of NPS-branded content):
|
||||
Adobe Systems). Both proprietary/commercially licensed, unlike the
|
||||
fonts this app already vendors (Noto Sans, SIL OFL — see
|
||||
`docs/third-party-notices.adoc`) — not something to pull in.
|
||||
* *A genuinely open alternative*: https://github.com/national-park-service/fonts
|
||||
— despite the org name, its own `DISCLAIMER.md` is explicit that this
|
||||
is an **independent, unofficial** project, "not affiliated with,
|
||||
endorsed by, or sponsored by" NPS, and that its six typefaces (NPS
|
||||
2026, Redwood Serif, Campmate Script, NPS Symbols, Sequoia Sans,
|
||||
Switchback) are original works "inspired by" public-lands signage —
|
||||
explicitly *not* reproductions of Rawlinson or anything NPS actually
|
||||
owns. SIL OFL 1.1 licensed, same family of license as the fonts this
|
||||
app already vendors. Unlike Rawlinson/Frutiger above, this one's
|
||||
actually usable if the NPS-inspired look is ever wanted in this app's
|
||||
own UI chrome (currently plain system fonts, `public/css/style.css`) —
|
||||
no current use case, but no licensing blocker either, worth
|
||||
remembering if that comes up.
|
||||
* *Hawaiian typography*: NPS Rawlinson includes vowels with Hawaiian
|
||||
macrons (ā, ē, ī, ō, ū) and the okina (ʻ); the regular weight of
|
||||
Frutiger doesn't, so NPS uses a separate "HFrutiger" weight for
|
||||
@@ -306,3 +319,27 @@ OSM-derived data, just a different source. No current need, and unlike
|
||||
everything else in this doc (CC0, BSD, MIT, public domain) this repo has
|
||||
**no `LICENSE` file at all** — its terms are genuinely unclear, so treat
|
||||
that as a blocker too, not just "no use case yet."
|
||||
|
||||
=== NPS Data API (developer.nps.gov)
|
||||
|
||||
A separate thing from the tile/Park Tiles service above — a general
|
||||
government open-data API (parks, alerts, campgrounds, places, ... —
|
||||
official docs: https://www.nps.gov/subjects/developer/api-documentation.htm,
|
||||
a JS-rendered Swagger UI that doesn't render via a plain fetch; a free
|
||||
API key from that same page is required to call it). No current use
|
||||
case (this app isn't NPS-park-specific — park4night POIs and personal
|
||||
GPX/FIT tracks, not NPS units), but it's the legitimate, non-Mapbox-Atlas
|
||||
government data source behind it, worth remembering if a "show nearby
|
||||
NPS park info/alerts" feature ever comes up — the `wms`/`source-arcgis-rest`
|
||||
npmap5-plugins entries above would be the MapLibre-side plumbing for it.
|
||||
Two hobby projects (both MIT, not NPS-affiliated, not directly reusable
|
||||
as libraries — noted only as real-world examples of consuming this API)
|
||||
turned up while looking at this:
|
||||
|
||||
* https://github.com/carlknutson/nps-changes-site — a React/Leaflet
|
||||
explorer for all NPS sites, most fields sourced from this API
|
||||
(supplemented by manual/AI-assisted research for fields the API
|
||||
doesn't have), data refreshed daily via GitHub Actions rather than
|
||||
called live.
|
||||
* https://github.com/seankelliher/us-national-parks-alerts — a
|
||||
Vue + Express directory of park alerts, pulled live through this API.
|
||||
+34
-10
@@ -31,6 +31,17 @@ const BADGE_IMAGE_PREFIX = "badge-"; // one registered image per distinct badge
|
||||
// one — both land on the same drop targets, told apart by this.
|
||||
const MOVE_MIME = "application/x-gpx-path";
|
||||
|
||||
// No server-rendered flag for this exists yet, and adding one would be more
|
||||
// plumbing than reusing what's already on the page: the topbar's own
|
||||
// logged_in check (views/index.erb) already renders a "Log out" link only
|
||||
// when logged in. Write-action controls (delete/download/upload/new
|
||||
// folder) check this to hide themselves entirely rather than being shown
|
||||
// and failing on click — login state never changes without a full page
|
||||
// reload, so this only needs checking once per render, not reactively.
|
||||
function isLoggedIn() {
|
||||
return !!document.querySelector('a[href="/logout"]');
|
||||
}
|
||||
|
||||
// A sidebar icon that opens a side panel for uploading GPX files (via
|
||||
// drag & drop, stored server-side in a folder hierarchy the user can add to
|
||||
// and reorganize by dragging), and toggling their tracks/waypoints on and
|
||||
@@ -109,6 +120,10 @@ export class GpxPanelControl {
|
||||
this._uploadStatus = this._panel.querySelector(".gpx-upload__status");
|
||||
this._panel.querySelector('[data-action="new-folder"]').innerHTML = NEW_FOLDER_ICON;
|
||||
this._panel.querySelector('[data-action="upload"]').innerHTML = UPLOAD_ICON;
|
||||
if (!isLoggedIn()) {
|
||||
this._panel.querySelector('[data-action="new-folder"]').hidden = true;
|
||||
this._panel.querySelector('[data-action="upload"]').hidden = true;
|
||||
}
|
||||
this._panel
|
||||
.querySelector(".gpx-panel__close")
|
||||
.addEventListener("click", () => this._togglePanel(false));
|
||||
@@ -249,7 +264,7 @@ export class GpxPanelControl {
|
||||
const name = document.createElement("span");
|
||||
name.textContent = folder.name;
|
||||
row.appendChild(name);
|
||||
row.appendChild(this._deleteButton(() => this._deleteFolder(folder.path)));
|
||||
if (isLoggedIn()) row.appendChild(this._deleteButton(() => this._deleteFolder(folder.path)));
|
||||
summary.appendChild(row);
|
||||
this._wireFolderDropTarget(summary, folder.path);
|
||||
|
||||
@@ -277,34 +292,43 @@ export class GpxPanelControl {
|
||||
// name reflects that (matching the real "<path>.geojson" cache file on
|
||||
// disk) rather than still reading as a plain, un-converted .gpx/.fit.
|
||||
// A direct .geojson upload already ends in .geojson, so it's left
|
||||
// alone. It's an <a>, not a <span>: clicking it downloads that GeoJSON
|
||||
// — the only way to get it out of the app, since the original .gpx/.fit
|
||||
// alone.
|
||||
const displayName = file.cached && !file.name.toLowerCase().endsWith(".geojson") ? `${file.name}.geojson` : file.name;
|
||||
const loggedIn = isLoggedIn();
|
||||
// Logged in: an <a>, not a <span> — clicking it downloads that GeoJSON,
|
||||
// the only way to get it out of the app, since the original .gpx/.fit
|
||||
// is never deleted but also never re-shown once converted. An <a>
|
||||
// nested in a <label> doesn't also toggle the checkbox on click (unlike
|
||||
// a plain element), so this doesn't fight the row's own
|
||||
// toggle-on-map behavior. href kept pointing at the real endpoint (so
|
||||
// hover/right-click/middle-click still do something sensible) but the
|
||||
// actual click goes through _download, which — like every other write
|
||||
// action in this file — needs to redirect to /login on a 401 rather
|
||||
// than navigate to a raw JSON error page (this endpoint now requires
|
||||
// login the same as upload/move/delete, see app.rb).
|
||||
const displayName = file.cached && !file.name.toLowerCase().endsWith(".geojson") ? `${file.name}.geojson` : file.name;
|
||||
const name = document.createElement("a");
|
||||
// actual click goes through _download, which redirects to /login on a
|
||||
// 401 rather than navigating to a raw JSON error page (this endpoint
|
||||
// requires login the same as upload/move/delete, see app.rb).
|
||||
// Logged out: plain text — nothing here would actually work, so it
|
||||
// isn't shown as if it would (same reasoning as the buttons below).
|
||||
const name = document.createElement(loggedIn ? "a" : "span");
|
||||
name.className = "gpx-tree__file-name";
|
||||
name.href = `${gpxFileUrl(file.path)}?download=geojson`;
|
||||
name.textContent = displayName;
|
||||
if (loggedIn) {
|
||||
name.href = `${gpxFileUrl(file.path)}?download=geojson`;
|
||||
name.title = `Download ${displayName}`;
|
||||
name.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
this._download(name.href, displayName);
|
||||
});
|
||||
}
|
||||
label.appendChild(checkbox);
|
||||
(file.kinds || []).forEach((kind) => label.appendChild(this._kindIcon(kind)));
|
||||
if (!file.cached) label.appendChild(this._pendingIcon());
|
||||
label.appendChild(name);
|
||||
row.appendChild(label);
|
||||
// Delete/download require login (app.rb); shown only once logged in
|
||||
// rather than shown-but-failing-on-click.
|
||||
if (loggedIn) {
|
||||
row.appendChild(this._downloadButton(file));
|
||||
row.appendChild(this._deleteButton(() => this._deleteFile(file.path)));
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user