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.
This commit is contained in:
1 parent
d1f2e7376e
commit
bc9ea58614
4 files changed
+112
-37
No files matched your search
+52
-6
@@ -14,16 +14,24 @@ html, body {
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 48px;
|
height: 44px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
background: #fff;
|
background: #1a1a1a;
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar__brand {
|
.topbar__brand {
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #333;
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar__coords {
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 12px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: #bbb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar__auth {
|
.topbar__auth {
|
||||||
@@ -32,7 +40,11 @@ html, body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #555;
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar__auth a {
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-body {
|
.app-body {
|
||||||
@@ -61,6 +73,17 @@ html, body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-group-label {
|
||||||
|
width: 28px;
|
||||||
|
height: 1px;
|
||||||
|
margin: 4px 0;
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-group-label:first-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-icon {
|
.sidebar-icon {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@@ -649,6 +672,29 @@ html, body {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gpx-popup__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin: -15px -10px 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpx-popup__header-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpx-popup__header-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.gpx-popup__time {
|
.gpx-popup__time {
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
+5
-2
@@ -916,9 +916,12 @@ function gpxFileUrl(path) {
|
|||||||
|
|
||||||
// properties come straight from an uploaded GPX file — untrusted input —
|
// properties come straight from an uploaded GPX file — untrusted input —
|
||||||
// so every value is escaped before landing in the popup's HTML.
|
// so every value is escaped before landing in the popup's HTML.
|
||||||
function popupHtml({ name, description, time, category, photos }) {
|
function popupHtml({ name, description, time, category, photos, icon }) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (name) parts.push(`<strong>${escapeHtml(name)}</strong>`);
|
if (name) {
|
||||||
|
const glyph = icon && POI_ICONS[icon] ? `<span class="gpx-popup__header-icon">${POI_ICONS[icon]}</span>` : "";
|
||||||
|
parts.push(`<div class="gpx-popup__header"><strong>${escapeHtml(name)}</strong>${glyph}</div>`);
|
||||||
|
}
|
||||||
if (category) parts.push(`<p class="gpx-popup__category">${escapeHtml(category)}</p>`);
|
if (category) parts.push(`<p class="gpx-popup__category">${escapeHtml(category)}</p>`);
|
||||||
if (photos?.length > 0) parts.push(formatPhotos(photos));
|
if (photos?.length > 0) parts.push(formatPhotos(photos));
|
||||||
if (description) parts.push(formatDescription(description));
|
if (description) parts.push(formatDescription(description));
|
||||||
|
|||||||
+54
-29
@@ -22,22 +22,23 @@ const ZOOM = 13;
|
|||||||
const ONLINE_STYLE_URL = "https://tiles.openfreemap.org/styles/liberty";
|
const ONLINE_STYLE_URL = "https://tiles.openfreemap.org/styles/liberty";
|
||||||
|
|
||||||
// Recolors OpenFreeMap Liberty to the same NPS-inspired palette as
|
// Recolors OpenFreeMap Liberty to the same NPS-inspired palette as
|
||||||
// OFFLINE_THEME below — warm cream ground, brown/khaki land cover, green
|
// OFFLINE_THEME below — warm cream ground, muted roads/boundaries — mapped
|
||||||
// reserved for parks, muted roads/boundaries — mapped onto Liberty's
|
// onto Liberty's OpenMapTiles layer ids instead of protomaps' theme roles
|
||||||
// OpenMapTiles layer ids instead of protomaps' theme roles (the two
|
// (the two schemas don't share layer names, so this is a separate
|
||||||
// schemas don't share layer names, so this is a separate hand-built
|
// hand-built table, not a shared object). Only the layers that dominate the
|
||||||
// table, not a shared object). Only the layers that dominate the map's
|
// map's overall color read are touched; labels, halos, rail, and minor
|
||||||
// overall color read are touched; labels, halos, rail, and minor
|
|
||||||
// overlays (cemetery/hospital/school/pitch/track shading, building
|
// overlays (cemetery/hospital/school/pitch/track shading, building
|
||||||
// outlines) are left at Liberty's own defaults — same "not a full
|
// outlines) are left at Liberty's own defaults — same "not a full
|
||||||
// recolor" scope as OFFLINE_THEME. Road casings have no protomaps-role
|
// recolor" scope as OFFLINE_THEME. Road casings have no protomaps-role
|
||||||
// equivalent to reuse (offline roads don't get a separate outline), so
|
// equivalent to reuse (offline roads don't get a separate outline), so
|
||||||
// #c9a66b there is reasoned to fit the family rather than sourced from
|
// #c9a66b there is reasoned to fit the family rather than sourced from
|
||||||
// anywhere real.
|
// anywhere real.
|
||||||
|
//
|
||||||
|
// wood/grass are deliberately left unpatched — Liberty's own green reads
|
||||||
|
// better than this app's earlier brown/khaki recolor, and OFFLINE_THEME's
|
||||||
|
// wood_a/scrub_a below were changed to match Liberty's green instead.
|
||||||
const ONLINE_STYLE_PATCH = {
|
const ONLINE_STYLE_PATCH = {
|
||||||
background: { "background-color": "#f9f6ef" },
|
background: { "background-color": "#f9f6ef" },
|
||||||
landcover_wood: { "fill-color": "#c9bd9c" },
|
|
||||||
landcover_grass: { "fill-color": "#d9cfb0" },
|
|
||||||
landcover_sand: { "fill-color": "#f0e4c4" },
|
landcover_sand: { "fill-color": "#f0e4c4" },
|
||||||
water: { "fill-color": "#a3cedf" },
|
water: { "fill-color": "#a3cedf" },
|
||||||
park: { "fill-color": "#b3e378" },
|
park: { "fill-color": "#b3e378" },
|
||||||
@@ -155,19 +156,25 @@ const OFFLINE_THEME = {
|
|||||||
background: "#f9f6ef", // real: Park Tiles' own background, hsl(40, 37%, 97%)
|
background: "#f9f6ef", // real: Park Tiles' own background, hsl(40, 37%, 97%)
|
||||||
earth: "#f9f6ef",
|
earth: "#f9f6ef",
|
||||||
water: "#a3cedf",
|
water: "#a3cedf",
|
||||||
// Brown/khaki, not green: in Park Tiles' own Imagery style, green is
|
// Matches OpenFreeMap Liberty's own wood/grass green (its landcover_wood
|
||||||
// specifically the protected-area/park color (below), not a general land
|
// hsla(98,61%,72%,0.7) and landcover_grass rgba(176,213,154,1) at 0.3
|
||||||
// cover fill — actual forest there is just the aerial photo underneath,
|
// opacity, blended over the cream background) — the online map's actual
|
||||||
// which this offline style has no equivalent of. Using that same vivid
|
// look whenever ONLINE_STYLE_PATCH's brown/khaki recolor fails to apply
|
||||||
// green for ordinary wood/scrub as well (an earlier version of this
|
// (see loadOnlineStyle's fetch fallback) turned out to be the look this
|
||||||
// theme did) made everything read as "green," not "brown with green
|
// app's users actually want, so offline was changed to match it instead
|
||||||
// parks" — reversed here to match the real distinction.
|
// of chasing the patch's own reliability.
|
||||||
wood_a: "#c9bd9c",
|
wood_a: "#c3e8ab",
|
||||||
wood_b: "#bcae8a",
|
wood_b: "#a8d888",
|
||||||
scrub_a: "#d9cfb0",
|
scrub_a: "#dcecc9",
|
||||||
scrub_b: "#cfc29d",
|
scrub_b: "#cfe3b8",
|
||||||
park_a: "#c8e2a0",
|
// Close to wood_b, not the more saturated Park Tiles green: protomaps'
|
||||||
park_b: "#b3e378", // real: Park Tiles' protected-area/park-point green
|
// theme routes "forest"/"nature_reserve"/"protected_area" (e.g. the Malé
|
||||||
|
// Karpaty CHKO boundary) through this same park_a/park_b pair as actual
|
||||||
|
// named parks — Liberty's online style doesn't make that reserve-boundary
|
||||||
|
// distinction at all, so a visibly different green here reads as an
|
||||||
|
// offline-only blotch rather than a deliberate distinction.
|
||||||
|
park_a: "#c3e8ab",
|
||||||
|
park_b: "#a8d888",
|
||||||
sand: "#f0e4c4",
|
sand: "#f0e4c4",
|
||||||
beach: "#f0e4c4",
|
beach: "#f0e4c4",
|
||||||
buildings: "#ded7cc",
|
buildings: "#ded7cc",
|
||||||
@@ -388,25 +395,43 @@ map.once("style.load", () => {
|
|||||||
map.addControl(new OverviewMapControl({}), "top-right");
|
map.addControl(new OverviewMapControl({}), "top-right");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const coordsLabel = document.querySelector(".topbar__coords");
|
||||||
|
const updateCoords = () => {
|
||||||
|
const { lng, lat } = map.getCenter();
|
||||||
|
coordsLabel.textContent = `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
|
||||||
|
};
|
||||||
|
updateCoords();
|
||||||
|
map.on("move", updateCoords);
|
||||||
|
|
||||||
const sidebar = document.getElementById("sidebar");
|
const sidebar = document.getElementById("sidebar");
|
||||||
|
|
||||||
|
// Grouped into the TODO's planned 4-module menu (Explorer / Offline hub /
|
||||||
|
// Navigation / Camper & HW) ahead of that split actually happening —
|
||||||
|
// Camper & HW has no sidebar item yet (leveling/solar/telemetry are all
|
||||||
|
// still unbuilt), so it's left out rather than shown empty.
|
||||||
const layerToggle = new LayerToggleControl(applyStyle);
|
const layerToggle = new LayerToggleControl(applyStyle);
|
||||||
|
const profilePanel = new TrackProfilePanel(map);
|
||||||
|
profilePanel.mount();
|
||||||
|
|
||||||
|
sidebar.insertAdjacentHTML("beforeend", `<div class="sidebar-group-label" title="Explorer"></div>`);
|
||||||
|
new GpxPanelControl(map, profilePanel).mount(sidebar);
|
||||||
|
|
||||||
|
sidebar.insertAdjacentHTML("beforeend", `<div class="sidebar-group-label" title="Offline hub"></div>`);
|
||||||
layerToggle.mount(sidebar);
|
layerToggle.mount(sidebar);
|
||||||
layerToggle.setActive("online");
|
layerToggle.setActive("online");
|
||||||
|
new RadarControl(map).mount(sidebar);
|
||||||
|
|
||||||
|
sidebar.insertAdjacentHTML("beforeend", `<div class="sidebar-group-label" title="Navigation"></div>`);
|
||||||
|
gnssControl.mount(sidebar);
|
||||||
|
new LiveTrackPanelControl(map).mount(sidebar);
|
||||||
|
|
||||||
// The sidebar already has a "Base map" icon (opens the same panel) —
|
// The sidebar already has a "Base map" icon (opens the same panel) —
|
||||||
// this is a second entry point for it, grouped with Home/zoom on the map
|
// this is a second entry point for it, grouped with Home/zoom on the map
|
||||||
// itself rather than off in the page's own side rail, matching NPS's
|
// itself rather than off in the page's own side rail, matching NPS's
|
||||||
// layout. Added after mount() since it reuses that panel/toggle state.
|
// layout. layerToggle only needs to exist (not be mounted yet) for this.
|
||||||
const mapLayersButton = new MapLayersButton(layerToggle);
|
const mapLayersButton = new MapLayersButton(layerToggle);
|
||||||
map.addControl(mapLayersButton, "top-left");
|
map.addControl(mapLayersButton, "top-left");
|
||||||
|
|
||||||
const profilePanel = new TrackProfilePanel(map);
|
|
||||||
profilePanel.mount();
|
|
||||||
new GpxPanelControl(map, profilePanel).mount(sidebar);
|
|
||||||
new RadarControl(map).mount(sidebar);
|
|
||||||
gnssControl.mount(sidebar);
|
|
||||||
new LiveTrackPanelControl(map).mount(sidebar);
|
|
||||||
|
|
||||||
function applyStyle(name) {
|
function applyStyle(name) {
|
||||||
layerToggle.setActive(name);
|
layerToggle.setActive(name);
|
||||||
map.setStyle(name === "offline" ? offlineStyle() : ONLINE_STYLE);
|
map.setStyle(name === "offline" ? offlineStyle() : ONLINE_STYLE);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<div id="app">
|
<div id="app">
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<span class="topbar__brand" title="rev <%= revision %>">MapTool</span>
|
<span class="topbar__brand" title="rev <%= revision %>">MapTool</span>
|
||||||
|
<span class="topbar__coords"></span>
|
||||||
<div class="topbar__auth">
|
<div class="topbar__auth">
|
||||||
<% if logged_in %>
|
<% if logged_in %>
|
||||||
<span><%= user %></span>
|
<span><%= user %></span>
|
||||||
|
|||||||
Reference in new issue
Block a user