Format the structured lines in park4night waypoint popups
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 16s

A park4night <desc> packs a free-text narrative and several lines of
structured data (address, rating, services, activities, the listing URL)
into one field, separated only by newlines. Rendered as a single paragraph
it read as a wall of run-together text — no visual hierarchy between the
actual description and what's really key-value metadata.

formatDescription now recognizes "Label: value" lines (Rating/Services/
Activities) and bare-URL lines, and gives each its own row instead of
burying them in the narrative paragraph: a bordered meta block with bold
labels, and an isolated link line. Anything that doesn't match — the real
narrative text, or a plain GPX file's <desc> with no such structure — stays
exactly as it rendered before, so this only adds structure, never removes
or reorders text.
This commit is contained in:
randy committed 2026-09-07 17:03:36 +02:00
1 parent 6f7d332993
commit 3759f7a5c9
3 files changed
+88 -2

No files matched your search

+15 -1
View File
@@ -315,7 +315,7 @@ Like the radar overlay, GPX layers are removed by `map.setStyle()`
(switching Online/Offline) and get re-added on `style.load` for (switching Online/Offline) and get re-added on `style.load` for
whichever files were checked. whichever files were checked.
Waypoint popups:: Clicking a waypoint circle opens a MapLibre popup Waypoint popups:: Clicking a waypoint badge opens a MapLibre popup
with its name, category (if any), description and time. Click/hover handling is with its name, category (if any), description and time. Click/hover handling is
registered once per file (via `map.on(type, layerId, handler)`, a registered once per file (via `map.on(type, layerId, handler)`, a
delegated listener that survives `setStyle()`) rather than on every delegated listener that survives `setStyle()`) rather than on every
@@ -328,6 +328,20 @@ splitting the text on a URL pattern and escaping every piece
assembling any HTML; the URL pattern excludes quote characters, so a assembling any HTML; the URL pattern excludes quote characters, so a
matched URL can never break out of the `href="..."` attribute. Every matched URL can never break out of the `href="..."` attribute. Every
other value is HTML-escaped before being handed to `Popup#setHTML`. other value is HTML-escaped before being handed to `Popup#setHTML`.
+
A park4night `<desc>` packs a free-text narrative and several lines of
structured data (address, rating, services, activities, the listing
URL) into that one field, separated only by newlines — rendered as a
single paragraph, it read as a wall of run-together text.
`formatDescription` pulls out whichever lines match a recognizable
`Label: value` shape (`Rating`/`Services`/`Activities`) or are a bare
URL, and renders each kind with its own visual treatment (a bordered
"meta" block with bold labels for the former, an isolated link line for
the latter); everything else — the actual narrative, or a plain GPX
file's `<desc>` with none of this structure at all — renders exactly as
before, one paragraph with line breaks. So this only ever adds
structure on top of recognized lines; it never loses or hides text from
files that don't have any.
Track properties:: Every track feature (both formats) also carries Track properties:: Every track feature (both formats) also carries
`distance_meters`, `elevation_gain_meters`, `duration_secs`, `distance_meters`, `elevation_gain_meters`, `duration_secs`,
+26
View File
@@ -450,6 +450,32 @@ html, body {
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
.gpx-popup__description {
line-height: 1.4;
}
.gpx-popup__meta {
margin-top: 8px;
padding-top: 6px;
border-top: 1px solid #eee;
font-size: 12px;
line-height: 1.5;
}
.gpx-popup__meta-row + .gpx-popup__meta-row {
margin-top: 2px;
}
.gpx-popup__meta-label {
font-weight: 600;
margin-right: 4px;
}
.gpx-popup__link {
margin-top: 6px;
font-size: 12px;
}
.gnss-control__status { .gnss-control__status {
color: #888; color: #888;
white-space: nowrap; white-space: nowrap;
+47 -1
View File
@@ -757,7 +757,7 @@ function popupHtml({ name, description, time, category }) {
const parts = []; const parts = [];
if (name) parts.push(`<strong>${escapeHtml(name)}</strong>`); if (name) parts.push(`<strong>${escapeHtml(name)}</strong>`);
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 (description) parts.push(`<p>${linkify(description).replace(/\n/g, "<br>")}</p>`); if (description) parts.push(formatDescription(description));
if (time) { if (time) {
const parsed = new Date(time); const parsed = new Date(time);
if (!Number.isNaN(parsed.getTime())) { if (!Number.isNaN(parsed.getTime())) {
@@ -795,6 +795,52 @@ function escapeHtml(text) {
return div.innerHTML; return div.innerHTML;
} }
// park4night exports pack a free-text description and several lines of
// structured data (address, rating, services, activities, the listing URL)
// into one <desc> blob, separated only by newlines — rendered as one plain
// paragraph, it read as a wall of run-together text. Lines matching a
// recognizable "Label: value" or bare-URL shape get pulled out and rendered
// as their own visually distinct rows instead; anything else (the actual
// narrative, or a plain GPX file's <desc> with no such structure at all)
// stays exactly as before, one paragraph with line breaks — so this only
// ever adds structure, never loses any text.
const DESCRIPTION_META_LINE = /^(Rating|Services|Activities):\s*(.+)$/i;
const DESCRIPTION_LINK_LINE = /^https?:\/\/\S+$/;
function formatDescription(description) {
const body = [];
const meta = [];
const links = [];
for (const line of description.split("\n")) {
const trimmed = line.trim();
const metaMatch = trimmed.match(DESCRIPTION_META_LINE);
if (DESCRIPTION_LINK_LINE.test(trimmed)) {
links.push(trimmed);
} else if (metaMatch) {
meta.push(metaMatch);
} else {
body.push(line);
}
}
const parts = [];
const bodyText = body.join("\n").trim();
if (bodyText) parts.push(`<p class="gpx-popup__description">${linkify(bodyText).replace(/\n/g, "<br>")}</p>`);
if (meta.length > 0) {
const rows = meta
.map(
([, label, value]) =>
`<div class="gpx-popup__meta-row"><span class="gpx-popup__meta-label">${escapeHtml(label)}</span> ${escapeHtml(value)}</div>`,
)
.join("");
parts.push(`<div class="gpx-popup__meta">${rows}</div>`);
}
links.forEach((url) => parts.push(`<p class="gpx-popup__link">${urlToLink(url)}</p>`));
return parts.join("");
}
// Turns bare URLs in untrusted text into safe <a> links: the string is // Turns bare URLs in untrusted text into safe <a> links: the string is
// split on the URL pattern and every piece is escaped — including the URL // split on the URL pattern and every piece is escaped — including the URL
// itself, both as href and as link text — before any HTML is assembled, so // itself, both as href and as link text — before any HTML is assembled, so