Show waypoint photos from newer p4n2gpx exports
A newer p4n2gpx run (Ruby rewrite, ~/test/mapping/p4n2gpx) started emitting one plain GPX <link href> per park4night listing photo, which this app parsed but never displayed. Gpx.waypoint_photos collects them into a photos property; the waypoint popup renders them as a row of thumbnails above the description, each opening full-size in a new tab. Absent on older exports and plain GPS waypoints, so the row just doesn't render for those.
This commit is contained in:
1 parent
dd272ffb8a
commit
dd0ad6d845
6 files changed
+72
-2
No files matched your search
+12
-1
@@ -415,7 +415,8 @@ Like the radar overlay, GPX layers are removed by `map.setStyle()`
|
|||||||
whichever files were checked.
|
whichever files were checked.
|
||||||
|
|
||||||
Waypoint popups:: Clicking a waypoint badge 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), photos (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
|
||||||
`style.load` re-add, which would otherwise stack duplicate handlers
|
`style.load` re-add, which would otherwise stack duplicate handlers
|
||||||
@@ -442,6 +443,16 @@ before, one paragraph with line breaks. So this only ever adds
|
|||||||
structure on top of recognized lines; it never loses or hides text from
|
structure on top of recognized lines; it never loses or hides text from
|
||||||
files that don't have any.
|
files that don't have any.
|
||||||
+
|
+
|
||||||
|
A newer p4n2gpx export also emits one plain GPX `<link href>` per
|
||||||
|
listing photo (not a vendor extension like `waypoint_style`'s
|
||||||
|
`<osmand:*>` — `<link>` is base GPX 1.1, any number allowed per
|
||||||
|
`<wpt>`/`<trk>`/`<rte>`). `Gpx.waypoint_photos` collects their `href`s
|
||||||
|
into a `photos` property; `formatPhotos` renders them as a row of
|
||||||
|
64px thumbnails above the description, each linking to the full-size
|
||||||
|
image in a new tab. A plain GPS waypoint or an older export has none,
|
||||||
|
so the row just doesn't render — same "only ever adds, never assumes"
|
||||||
|
approach as the description formatting above.
|
||||||
|
+
|
||||||
`Services`/`Activities` get one further step (`formatMetaRow`,
|
`Services`/`Activities` get one further step (`formatMetaRow`,
|
||||||
`formatTags`): the value is park4night's own comma-separated codes
|
`formatTags`): the value is park4night's own comma-separated codes
|
||||||
(`animaux, point_eau, wc_public`), which on their own told a reader
|
(`animaux, point_eau, wc_public`), which on their own told a reader
|
||||||
|
|||||||
@@ -51,11 +51,21 @@ module Maptool
|
|||||||
"category" => child_text(wpt, "type"),
|
"category" => child_text(wpt, "type"),
|
||||||
"color" => ext[:color],
|
"color" => ext[:color],
|
||||||
"icon" => ext[:icon],
|
"icon" => ext[:icon],
|
||||||
|
"photos" => waypoint_photos(wpt),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
private_class_method :waypoint_feature
|
private_class_method :waypoint_feature
|
||||||
|
|
||||||
|
# <link> is plain GPX 1.1 (a wpt/trk/rte can carry any number of them),
|
||||||
|
# not a vendor extension like waypoint_style's <osmand:*> — a newer
|
||||||
|
# p4n2gpx export started emitting one per park4night listing photo. A
|
||||||
|
# plain GPS waypoint has none, so this is almost always empty.
|
||||||
|
def self.waypoint_photos(wpt)
|
||||||
|
wpt.get_elements("link").filter_map { |link| link.attributes["href"] }
|
||||||
|
end
|
||||||
|
private_class_method :waypoint_photos
|
||||||
|
|
||||||
OSMAND_NAMESPACE = "https://osmand.net"
|
OSMAND_NAMESPACE = "https://osmand.net"
|
||||||
private_constant :OSMAND_NAMESPACE
|
private_constant :OSMAND_NAMESPACE
|
||||||
|
|
||||||
|
|||||||
@@ -582,6 +582,21 @@ html, body {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gpx-popup__photos {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 4px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gpx-popup__photo {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.gpx-popup__meta {
|
.gpx-popup__meta {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
|
|||||||
+18
-1
@@ -754,10 +754,11 @@ 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 }) {
|
function popupHtml({ name, description, time, category, photos }) {
|
||||||
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 (photos?.length > 0) parts.push(formatPhotos(photos));
|
||||||
if (description) parts.push(formatDescription(description));
|
if (description) parts.push(formatDescription(description));
|
||||||
if (time) {
|
if (time) {
|
||||||
const parsed = new Date(time);
|
const parsed = new Date(time);
|
||||||
@@ -808,6 +809,22 @@ function escapeHtml(text) {
|
|||||||
const DESCRIPTION_META_LINE = /^(Rating|Services|Activities):\s*(.+)$/i;
|
const DESCRIPTION_META_LINE = /^(Rating|Services|Activities):\s*(.+)$/i;
|
||||||
const DESCRIPTION_LINK_LINE = /^https?:\/\/\S+$/;
|
const DESCRIPTION_LINK_LINE = /^https?:\/\/\S+$/;
|
||||||
|
|
||||||
|
// <link> photo URLs (a newer p4n2gpx export adds one per park4night listing
|
||||||
|
// photo — see lib/maptool/gpx.rb's waypoint_photos) as a row of thumbnails,
|
||||||
|
// each opening the full-size image in a new tab. Untrusted URLs from an
|
||||||
|
// uploaded file, same as everywhere else in this popup — escaped for use as
|
||||||
|
// both an href and an img src.
|
||||||
|
function formatPhotos(photos) {
|
||||||
|
const thumbs = photos
|
||||||
|
.map((url) => escapeHtml(url))
|
||||||
|
.map(
|
||||||
|
(url) =>
|
||||||
|
`<a href="${url}" target="_blank" rel="noopener noreferrer"><img class="gpx-popup__photo" src="${url}" loading="lazy" alt=""></a>`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
return `<div class="gpx-popup__photos">${thumbs}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatDescription(description) {
|
function formatDescription(description) {
|
||||||
const body = [];
|
const body = [];
|
||||||
const meta = [];
|
const meta = [];
|
||||||
|
|||||||
Vendored
+5
@@ -20,4 +20,9 @@
|
|||||||
<locus:icon xmlns:locus="http://www.locusmap.eu">res/drawable-hdpi/ic_parking_small.png</locus:icon>
|
<locus:icon xmlns:locus="http://www.locusmap.eu">res/drawable-hdpi/ic_parking_small.png</locus:icon>
|
||||||
</extensions>
|
</extensions>
|
||||||
</wpt>
|
</wpt>
|
||||||
|
<wpt lat="48.103" lon="17.203">
|
||||||
|
<name>With photos</name>
|
||||||
|
<link href="https://cdn3.park4night.com/lieu/1_100/1_gd.jpg"><text>Photo</text></link>
|
||||||
|
<link href="https://cdn3.park4night.com/lieu/1_100/2_gd.jpg"><text>Photo</text></link>
|
||||||
|
</wpt>
|
||||||
</gpx>
|
</gpx>
|
||||||
@@ -74,6 +74,18 @@ class GpxTest < Minitest::Test
|
|||||||
assert_nil plain["properties"]["icon"]
|
assert_nil plain["properties"]["icon"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_waypoint_captures_link_hrefs_as_photos
|
||||||
|
geojson = Maptool::Gpx.to_geojson(fixture_path("sample_poi.gpx"))
|
||||||
|
with_photos = geojson["features"].find { |f| f["properties"]["name"] == "With photos" }
|
||||||
|
plain = geojson["features"].find { |f| f["properties"]["name"] == "Plain waypoint" }
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
["https://cdn3.park4night.com/lieu/1_100/1_gd.jpg", "https://cdn3.park4night.com/lieu/1_100/2_gd.jpg"],
|
||||||
|
with_photos["properties"]["photos"]
|
||||||
|
)
|
||||||
|
assert_empty plain["properties"]["photos"]
|
||||||
|
end
|
||||||
|
|
||||||
def test_kinds_detects_routes
|
def test_kinds_detects_routes
|
||||||
assert_equal ["route"], Maptool::Gpx.kinds(fixture_path("sample_route.gpx"))
|
assert_equal ["route"], Maptool::Gpx.kinds(fixture_path("sample_route.gpx"))
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in new issue
Block a user