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 :public_folder, File.join(__dir__, "public")
|
||||||
set :views, File.join(__dir__, "views")
|
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
|
# A persistent secret across restarts/redeploys — otherwise every deploy
|
||||||
# would invalidate every logged-in session's cookie (see
|
# would invalidate every logged-in session's cookie (see
|
||||||
# docs/deployment.adoc). Falls back to a random one so the app still
|
# 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
|
exporting a file (`GET /api/gpx/file/*?download=...`), even though
|
||||||
plain viewing of that same URL (no `?download`) doesn't; not a write
|
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.
|
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
|
Login delegates to the Gitea instance at `git.priesol.net` via OAuth2
|
||||||
(`GET /login` redirects to Gitea's authorize endpoint, `GET
|
(`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
|
Adobe Systems). Both proprietary/commercially licensed, unlike the
|
||||||
fonts this app already vendors (Noto Sans, SIL OFL — see
|
fonts this app already vendors (Noto Sans, SIL OFL — see
|
||||||
`docs/third-party-notices.adoc`) — not something to pull in.
|
`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
|
* *Hawaiian typography*: NPS Rawlinson includes vowels with Hawaiian
|
||||||
macrons (ā, ē, ī, ō, ū) and the okina (ʻ); the regular weight of
|
macrons (ā, ē, ī, ō, ū) and the okina (ʻ); the regular weight of
|
||||||
Frutiger doesn't, so NPS uses a separate "HFrutiger" weight for
|
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
|
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
|
**no `LICENSE` file at all** — its terms are genuinely unclear, so treat
|
||||||
that as a blocker too, not just "no use case yet."
|
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.
|
||||||
+41
-17
@@ -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.
|
// one — both land on the same drop targets, told apart by this.
|
||||||
const MOVE_MIME = "application/x-gpx-path";
|
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
|
// 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
|
// 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
|
// 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._uploadStatus = this._panel.querySelector(".gpx-upload__status");
|
||||||
this._panel.querySelector('[data-action="new-folder"]').innerHTML = NEW_FOLDER_ICON;
|
this._panel.querySelector('[data-action="new-folder"]').innerHTML = NEW_FOLDER_ICON;
|
||||||
this._panel.querySelector('[data-action="upload"]').innerHTML = UPLOAD_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
|
this._panel
|
||||||
.querySelector(".gpx-panel__close")
|
.querySelector(".gpx-panel__close")
|
||||||
.addEventListener("click", () => this._togglePanel(false));
|
.addEventListener("click", () => this._togglePanel(false));
|
||||||
@@ -249,7 +264,7 @@ export class GpxPanelControl {
|
|||||||
const name = document.createElement("span");
|
const name = document.createElement("span");
|
||||||
name.textContent = folder.name;
|
name.textContent = folder.name;
|
||||||
row.appendChild(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);
|
summary.appendChild(row);
|
||||||
this._wireFolderDropTarget(summary, folder.path);
|
this._wireFolderDropTarget(summary, folder.path);
|
||||||
|
|
||||||
@@ -277,34 +292,43 @@ export class GpxPanelControl {
|
|||||||
// name reflects that (matching the real "<path>.geojson" cache file on
|
// name reflects that (matching the real "<path>.geojson" cache file on
|
||||||
// disk) rather than still reading as a plain, un-converted .gpx/.fit.
|
// disk) rather than still reading as a plain, un-converted .gpx/.fit.
|
||||||
// A direct .geojson upload already ends in .geojson, so it's left
|
// 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
|
// alone.
|
||||||
// — the only way to get it out of the app, since the original .gpx/.fit
|
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>
|
// 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
|
// 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
|
// a plain element), so this doesn't fight the row's own
|
||||||
// toggle-on-map behavior. href kept pointing at the real endpoint (so
|
// toggle-on-map behavior. href kept pointing at the real endpoint (so
|
||||||
// hover/right-click/middle-click still do something sensible) but the
|
// hover/right-click/middle-click still do something sensible) but the
|
||||||
// actual click goes through _download, which — like every other write
|
// actual click goes through _download, which redirects to /login on a
|
||||||
// action in this file — needs to redirect to /login on a 401 rather
|
// 401 rather than navigating to a raw JSON error page (this endpoint
|
||||||
// than navigate to a raw JSON error page (this endpoint now requires
|
// requires login the same as upload/move/delete, see app.rb).
|
||||||
// login the same as upload/move/delete, see app.rb).
|
// Logged out: plain text — nothing here would actually work, so it
|
||||||
const displayName = file.cached && !file.name.toLowerCase().endsWith(".geojson") ? `${file.name}.geojson` : file.name;
|
// isn't shown as if it would (same reasoning as the buttons below).
|
||||||
const name = document.createElement("a");
|
const name = document.createElement(loggedIn ? "a" : "span");
|
||||||
name.className = "gpx-tree__file-name";
|
name.className = "gpx-tree__file-name";
|
||||||
name.href = `${gpxFileUrl(file.path)}?download=geojson`;
|
|
||||||
name.textContent = displayName;
|
name.textContent = displayName;
|
||||||
name.title = `Download ${displayName}`;
|
if (loggedIn) {
|
||||||
name.addEventListener("click", (event) => {
|
name.href = `${gpxFileUrl(file.path)}?download=geojson`;
|
||||||
event.preventDefault();
|
name.title = `Download ${displayName}`;
|
||||||
this._download(name.href, displayName);
|
name.addEventListener("click", (event) => {
|
||||||
});
|
event.preventDefault();
|
||||||
|
this._download(name.href, displayName);
|
||||||
|
});
|
||||||
|
}
|
||||||
label.appendChild(checkbox);
|
label.appendChild(checkbox);
|
||||||
(file.kinds || []).forEach((kind) => label.appendChild(this._kindIcon(kind)));
|
(file.kinds || []).forEach((kind) => label.appendChild(this._kindIcon(kind)));
|
||||||
if (!file.cached) label.appendChild(this._pendingIcon());
|
if (!file.cached) label.appendChild(this._pendingIcon());
|
||||||
label.appendChild(name);
|
label.appendChild(name);
|
||||||
row.appendChild(label);
|
row.appendChild(label);
|
||||||
row.appendChild(this._downloadButton(file));
|
// Delete/download require login (app.rb); shown only once logged in
|
||||||
row.appendChild(this._deleteButton(() => this._deleteFile(file.path)));
|
// rather than shown-but-failing-on-click.
|
||||||
|
if (loggedIn) {
|
||||||
|
row.appendChild(this._downloadButton(file));
|
||||||
|
row.appendChild(this._deleteButton(() => this._deleteFile(file.path)));
|
||||||
|
}
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user