From 3759f7a5c932250a9523a093758e4a8d6e1cefe9 Mon Sep 17 00:00:00 2001 From: Roman Priesol Date: Mon, 7 Sep 2026 17:03:36 +0200 Subject: [PATCH] Format the structured lines in park4night waypoint popups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A park4night 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 with no such structure — stays exactly as it rendered before, so this only adds structure, never removes or reorders text. --- docs/architecture.adoc | 16 +++++++++++++- public/css/style.css | 26 +++++++++++++++++++++++ public/js/gpx.js | 48 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/docs/architecture.adoc b/docs/architecture.adoc index 0dd2ca7..72b118c 100644 --- a/docs/architecture.adoc +++ b/docs/architecture.adoc @@ -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 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 registered once per file (via `map.on(type, layerId, handler)`, a 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 matched URL can never break out of the `href="..."` attribute. Every other value is HTML-escaped before being handed to `Popup#setHTML`. ++ +A park4night `` 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 `` 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 `distance_meters`, `elevation_gain_meters`, `duration_secs`, diff --git a/public/css/style.css b/public/css/style.css index b914c49..b279c3a 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -450,6 +450,32 @@ html, body { 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 { color: #888; white-space: nowrap; diff --git a/public/js/gpx.js b/public/js/gpx.js index f17b977..915af09 100644 --- a/public/js/gpx.js +++ b/public/js/gpx.js @@ -757,7 +757,7 @@ function popupHtml({ name, description, time, category }) { const parts = []; if (name) parts.push(`${escapeHtml(name)}`); if (category) parts.push(`

${escapeHtml(category)}

`); - if (description) parts.push(`

${linkify(description).replace(/\n/g, "
")}

`); + if (description) parts.push(formatDescription(description)); if (time) { const parsed = new Date(time); if (!Number.isNaN(parsed.getTime())) { @@ -795,6 +795,52 @@ function escapeHtml(text) { 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 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 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(`

${linkify(bodyText).replace(/\n/g, "
")}

`); + if (meta.length > 0) { + const rows = meta + .map( + ([, label, value]) => + `
${escapeHtml(label)} ${escapeHtml(value)}
`, + ) + .join(""); + parts.push(`
${rows}
`); + } + links.forEach((url) => parts.push(``)); + + return parts.join(""); +} + // Turns bare URLs in untrusted text into safe links: the string is // 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