"use strict";
let trail_chart_number = 0;
const create_map = (map_element) => {
const open_street_map_mapnik = L.tileLayer.provider("OpenStreetMap.Mapnik", {
referrerPolicy: "strict-origin-when-cross-origin",
});
const open_street_map_de = L.tileLayer.provider("OpenStreetMap.DE", {
referrerPolicy: "strict-origin-when-cross-origin",
});
const open_street_map_france = L.tileLayer.provider("OpenStreetMap.France", {
referrerPolicy: "strict-origin-when-cross-origin",
});
const opnv_karte = L.tileLayer.provider("OPNVKarte", {
referrerPolicy: "strict-origin-when-cross-origin",
});
const open_topo_map = L.tileLayer.provider("OpenTopoMap", {
referrerPolicy: "strict-origin-when-cross-origin",
});
const cycl_osm = L.tileLayer.provider("CyclOSM", {
referrerPolicy: "strict-origin-when-cross-origin",
});
const mtb_map = L.tileLayer.provider("MtbMap", {
referrerPolicy: "strict-origin-when-cross-origin",
});
const map = L.map(map_element, {
center: [49.75, 6.1],
zoom: 8,
zoomControl: true,
scrollWheelZoom: true,
keyboard: true,
layers: [open_street_map_mapnik],
});
L.control
.layers(
{
OpenStreetMap: open_street_map_mapnik,
OpenStreetMapDE: open_street_map_de,
OpenStreetMapFrance: open_street_map_france,
OPNVKarte: opnv_karte,
OpenTopoMap: open_topo_map,
CycleOSM: cycl_osm,
MtbMap: mtb_map,
},
null,
{
collapsed: true,
position: "topright",
},
)
.addTo(map);
L.control
.scale({
imperial: false,
metric: true,
position: "bottomleft",
})
.addTo(map);
new L.Control.FullScreen({
position: "topleft",
title: "View map fullscreen",
titleCancel: "Exit fullscreen",
}).addTo(map);
return map;
};
const create_parking_icon = () =>
L.icon({
iconUrl: "/static/images/trail/parking.svg",
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
tooltipAnchor: [0, -28],
className: "trail-parking-marker",
});
const create_display_control = (map, chart_element) => {
if (!chart_element.id) {
trail_chart_number += 1;
chart_element.id = `trail-chart-${trail_chart_number}`;
}
const parking_icon = create_parking_icon();
const display_control = L.control.elevation({
theme: "trail-theme",
detached: true,
elevationDiv: `#${chart_element.id}`,
altitude: true,
distance: true,
time: "summary",
speed: "summary",
pace: "summary",
slope: "summary",
acceleration: false,
summary: false,
downloadLink: false,
legend: true,
ruler: false,
autofitBounds: true,
followMarker: false,
marker: "position-marker",
almostOver: false,
distanceMarkers: false,
edgeScale: false,
hotline: false,
waypoints: "markers",
wptLabels: "markers",
wptIcons: {
Parking: parking_icon,
parking: parking_icon,
"Parking Area": parking_icon,
"parking area": parking_icon,
"parking-area": parking_icon,
Car: parking_icon,
car: parking_icon,
},
trkStart: false,
trkEnd: false,
closeBtn: false,
});
display_control.addTo(map);
return display_control;
};
const create_summary_control = () => {
const map_element = document.createElement("div");
map_element.hidden = true;
document.body.append(map_element);
const map = L.map(map_element, {
center: [0, 0],
zoom: 1,
zoomControl: false,
attributionControl: false,
keyboard: false,
dragging: false,
scrollWheelZoom: false,
doubleClickZoom: false,
boxZoom: false,
touchZoom: false,
});
const summary_control = L.control.elevation({
detached: false,
collapsed: true,
altitude: "summary",
distance: "summary",
time: "summary",
speed: "summary",
pace: "summary",
slope: "summary",
acceleration: false,
summary: false,
downloadLink: false,
legend: false,
ruler: false,
autofitBounds: false,
followMarker: false,
marker: false,
almostOver: false,
distanceMarkers: false,
edgeScale: false,
hotline: false,
waypoints: false,
wptLabels: false,
trkStart: false,
trkEnd: false,
closeBtn: false,
});
summary_control.addTo(map);
return {
control: summary_control,
destroy: () => {
summary_control.remove();
map.remove();
map_element.remove();
},
};
};
function visit_leaflet_layer(layer, callback) {
if (!layer) {
return;
}
if (typeof layer.eachLayer === "function") {
layer.eachLayer((child_layer) => {
visit_leaflet_layer(child_layer, callback);
});
return;
}
callback(layer);
}
const create_trail_icon = (type, label) =>
L.divIcon({
className: `trail-marker trail-marker-${type}`,
html:
'' +
`${label}`,
iconSize: [30, 30],
iconAnchor: [15, 15],
popupAnchor: [0, -16],
tooltipAnchor: [0, -16],
});
function add_trail_marker(map, position, type, label) {
const marker = L.marker(position, {
icon: create_trail_icon(type, label),
keyboard: true,
title: label,
alt: label,
});
marker.bindTooltip(label, {
direction: "top",
});
marker.addTo(map);
}
const add_endpoint_markers = (map, loaded_layer) => {
const lat_lngs = [];
visit_leaflet_layer(loaded_layer, (layer) => {
if (typeof layer.getLatLngs !== "function") {
return;
}
lat_lngs.push(
...layer
.getLatLngs()
.flat(Infinity)
.filter(
(item) =>
item &&
typeof item.lat === "number" &&
typeof item.lng === "number",
),
);
});
if (lat_lngs.length < 2) {
console.warn(
"The loaded GPX did not contain enough track points for endpoint markers.",
);
return;
}
const start = lat_lngs[0];
const finish = lat_lngs[lat_lngs.length - 1];
if (start.distanceTo(finish) <= 50) {
add_trail_marker(map, start, "loop", "Start and finish");
return;
}
add_trail_marker(map, start, "start", "Start");
add_trail_marker(map, finish, "finish", "Finish");
};
const format_duration = (milliseconds) => {
if (!Number.isFinite(milliseconds) || milliseconds < 0) {
return "–";
}
const total_seconds = Math.floor(milliseconds / 1000);
const days = Math.floor(total_seconds / 86400);
const hours = Math.floor((total_seconds % 86400) / 3600);
const minutes = Math.floor((total_seconds % 3600) / 60);
const seconds = total_seconds % 60;
const time = [hours, minutes, seconds]
.map((value) => String(value).padStart(2, "0"))
.join(":");
if (days === 0) {
return time;
}
const day_label = days === 1 ? "day" : "days";
return `${days} ${day_label} ${time}`;
};
const format_pace = (time_milliseconds, distance_kilometres) => {
if (
!Number.isFinite(time_milliseconds) ||
!Number.isFinite(distance_kilometres) ||
time_milliseconds < 0 ||
distance_kilometres <= 0
) {
return "–";
}
const seconds_per_kilometre = Math.round(
time_milliseconds / 1000 / distance_kilometres,
);
const minutes = Math.floor(seconds_per_kilometre / 60);
const seconds = seconds_per_kilometre % 60;
return `${minutes}:${String(seconds).padStart(2, "0")} min/km`;
};
const initialize_trail_map = (trail) => {
const map_element = trail.querySelector("[data-trail-map]");
const chart_element = trail.querySelector("[data-trail-chart]");
const message_element = trail.querySelector("[data-trail-message]");
const trail_gpx_url = trail.dataset.trailGpxUrl;
const activity_gpx_url = trail.dataset.activityGpxUrl;
if (!map_element || !chart_element || (!trail_gpx_url && !activity_gpx_url)) {
console.error(
"Could not initialize trail map.",
new Error(
"The trail block requires a map, a chart and at least one GPX file.",
),
);
if (map_element) {
map_element.hidden = true;
}
if (chart_element) {
chart_element.hidden = true;
}
if (message_element) {
message_element.hidden = false;
}
return;
}
const display_gpx_url = trail_gpx_url || activity_gpx_url;
const summary_gpx_url = activity_gpx_url || trail_gpx_url;
const summary_uses_separate_gpx = summary_gpx_url !== display_gpx_url;
try {
const map = create_map(map_element);
const display_control = create_display_control(map, chart_element);
const set_summary_values = (track_info) => {
const set_value = (selector, value) => {
const element = trail.querySelector(selector);
if (element) {
element.textContent = value;
}
};
set_value(
"[data-trail-distance]",
`${track_info.distance.toFixed(2)} km`,
);
set_value("[data-trail-time]", format_duration(track_info.time));
set_value(
"[data-trail-pace]",
format_pace(track_info.time, track_info.distance),
);
set_value(
"[data-trail-maximum-elevation]",
`${track_info.elevation_max.toFixed(0)} m`,
);
set_value(
"[data-trail-minimum-elevation]",
`${track_info.elevation_min.toFixed(0)} m`,
);
set_value(
"[data-trail-average-elevation]",
`${track_info.elevation_avg.toFixed(0)} m`,
);
set_value("[data-trail-ascent]", `${track_info.ascent.toFixed(0)} m`);
set_value("[data-trail-descent]", `${track_info.descent.toFixed(0)} m`);
set_value(
"[data-trail-average-speed]",
`${track_info.speed_avg.toFixed(2)} km/h`,
);
set_value(
"[data-trail-maximum-speed]",
`${track_info.speed_max.toFixed(2)} km/h`,
);
};
display_control.once("eledata_loaded", ({ layer, track_info }) => {
add_endpoint_markers(map, layer);
if (!summary_uses_separate_gpx) {
set_summary_values(track_info);
}
map.invalidateSize(false);
display_control.redraw();
});
display_control.load(display_gpx_url);
if (summary_uses_separate_gpx) {
const { control: summary_control, destroy: destroy_summary_control } =
create_summary_control();
summary_control.once("eledata_loaded", ({ track_info }) => {
set_summary_values(track_info);
setTimeout(() => {
destroy_summary_control();
}, 0);
});
summary_control.load(summary_gpx_url);
}
} catch (error) {
console.error("Could not initialize trail map.", error);
map_element.hidden = true;
chart_element.hidden = true;
if (message_element) {
message_element.hidden = false;
}
}
};
document.addEventListener("DOMContentLoaded", () => {
L.registerLocale("en:trail", {
"x: ": "Distance: ",
"y: ": "Elevation: ",
Altitude: "Elevation",
});
L.setLocale("en:trail");
const trails = document.querySelectorAll(".trail");
for (const trail of trails) {
initialize_trail_map(trail);
}
});