server_photo_s85js/public/js/map/activitiesLayer.js
2026-08-11 08:45:04 +02:00

604 lines
14 KiB
JavaScript

// public/js/mapActivitiesLayer.js
// ===============================
// LAYER MAPPA ATTIVITÀ
// ===============================
(function () {
const ACTIVITY_ROUTE_COLORS = [
"#e6194b",
"#3cb44b",
"#4363d8",
"#f58231",
"#911eb4",
"#46f0f0",
"#f032e6",
"#bcf60c",
"#008080",
"#9a6324",
"#800000",
"#000075",
"#ff6b00",
"#00a8e8",
"#8d6e63",
"#2e7d32",
];
const MAX_GLOBAL_ACTIVITY_ROUTES = 80;
let activityRoutesLayer = null;
let selectedActivityLayer = null;
let selectedActivityEndMarker = null;
let showAllActivityRoutes = false;
let cachedRouteData = [];
let lastRenderToken = 0;
// ===============================
// CLUSTER ATTIVITÀ
// ===============================
window.createActivityClusterIcon = function (cluster) {
const count = cluster.getChildCount();
return L.divIcon({
html: `
<div class="activity-cluster">
${count}
</div>
`,
className: "marker-cluster-wrapper",
iconSize: L.point(48, 48),
});
};
// ===============================
// PULIZIA LAYER ATTIVITÀ
// ===============================
window.clearActivityMapLayers = function () {
if (activityRoutesLayer) {
activityRoutesLayer.clearLayers();
}
if (selectedActivityLayer) {
selectedActivityLayer.remove();
selectedActivityLayer = null;
}
if (selectedActivityEndMarker) {
selectedActivityEndMarker.remove();
selectedActivityEndMarker = null;
}
};
// ===============================
// TOGGLE TRACCE
// ===============================
function ensureActivityRoutesToggle() {
let btn = document.getElementById("toggleActivityRoutesBtn");
if (btn) return btn;
btn = document.createElement("button");
btn.id = "toggleActivityRoutesBtn";
btn.type = "button";
btn.className = "map-toggle-routes";
btn.textContent = "Mostra tracce";
btn.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
showAllActivityRoutes = !showAllActivityRoutes;
btn.classList.toggle("active", showAllActivityRoutes);
btn.textContent = showAllActivityRoutes
? "Nascondi tracce"
: "Mostra tracce";
await renderAllActivityRoutes();
});
document.getElementById("globalMap")?.appendChild(btn);
return btn;
}
window.hideActivityRoutesToggle = function () {
const btn = document.getElementById("toggleActivityRoutesBtn");
if (btn) {
btn.style.display = "none";
}
showAllActivityRoutes = false;
};
function showActivityRoutesToggle() {
const btn = ensureActivityRoutesToggle();
btn.style.display = "inline-flex";
btn.classList.toggle("active", showAllActivityRoutes);
btn.textContent = showAllActivityRoutes
? "Nascondi tracce"
: "Mostra tracce";
}
// ===============================
// ICONA MARKER ATTIVITÀ
// ===============================
function createActivityStartIcon(item) {
const sport = String(item.sport || "").toLowerCase();
let emoji = "●";
let cls = "activity-marker-generic";
if (sport.includes("run") || sport.includes("corsa")) {
emoji = "🏃";
cls = "activity-marker-run";
} else if (
sport.includes("bike") ||
sport.includes("bici") ||
sport.includes("cycle")
) {
emoji = "🚴";
cls = "activity-marker-bike";
} else if (sport.includes("walk") || sport.includes("cammin")) {
emoji = "🚶";
cls = "activity-marker-walk";
}
return L.divIcon({
html: `
<div class="activity-start-marker ${cls}">
<span>${emoji}</span>
</div>
`,
className: "marker-cluster-wrapper",
iconSize: L.point(42, 42),
iconAnchor: L.point(21, 21),
});
}
// ===============================
// RENDER ATTIVITÀ
// ===============================
window.renderActivityMapLayer = async function () {
if (!window.globalMap || !window.globalMarkers) return;
const token = ++lastRenderToken;
showActivityRoutesToggle();
if (!activityRoutesLayer) {
activityRoutesLayer = L.layerGroup().addTo(window.globalMap);
}
activityRoutesLayer.clearLayers();
clearSelectedActivityOnly();
const items = getLocalItems();
const activityItems = items.filter(isProbablyActivity);
cachedRouteData = [];
const routeDataList = await Promise.all(
activityItems.map(async (item, index) => {
const points = await getRoutePointsSafe(item);
if (!points || points.length < 2) {
return null;
}
const latlngs = pointsToLatLngs(points);
if (latlngs.length < 2) {
return null;
}
return {
key: getActivityKey(item, index),
index,
item,
points,
latlngs,
start: latlngs[0],
end: latlngs[latlngs.length - 1],
cells: getRouteCells(points),
};
}),
);
if (token !== lastRenderToken) return;
cachedRouteData = routeDataList.filter(Boolean);
cachedRouteData.forEach((route) => {
const marker = L.marker(route.start, {
icon: createActivityStartIcon(route.item),
title:
route.item.display_name ||
route.item.route_name ||
route.item.name ||
route.item.file_name ||
"",
});
marker.__item = route.item;
marker.__routeData = route;
marker.on("click", () => {
showSelectedActivityOnGlobalMap(route);
});
window.MapEngine?.addMarker(marker);
});
if (cachedRouteData.length > 0) {
const bounds = L.latLngBounds(
cachedRouteData.map((route) => route.start),
);
if (bounds.isValid()) {
window.globalMap.fitBounds(bounds, {
padding: [60, 60],
maxZoom: 12,
});
}
}
if (showAllActivityRoutes) {
await renderAllActivityRoutes();
}
};
// ===============================
// TRACCIA SELEZIONATA
// ===============================
function showSelectedActivityOnGlobalMap(route) {
clearSelectedActivityOnly();
selectedActivityLayer = L.polyline(route.latlngs, {
color: "#ff6b00",
weight: 5,
opacity: 1,
lineCap: "round",
lineJoin: "round",
}).addTo(window.globalMap);
selectedActivityLayer.on("click", () => {
window.openActivityModal?.(route.item);
});
selectedActivityEndMarker = L.circleMarker(route.end, {
radius: 7,
color: "#b00020",
weight: 2,
fillColor: "#e32636",
fillOpacity: 1,
})
.bindTooltip("Arrivo")
.addTo(window.globalMap);
selectedActivityEndMarker.on("click", () => {
window.openActivityModal?.(route.item);
});
const popupHtml = createActivityPopupHtml(route.item);
selectedActivityLayer.bindPopup(popupHtml).openPopup();
window.globalMap.fitBounds(selectedActivityLayer.getBounds(), {
padding: [50, 50],
maxZoom: 15,
});
setTimeout(() => {
const btn = document.querySelector(
`[data-open-activity-key="${route.key}"]`,
);
btn?.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
window.openActivityModal?.(route.item);
});
}, 0);
}
function clearSelectedActivityOnly() {
if (selectedActivityLayer) {
selectedActivityLayer.remove();
selectedActivityLayer = null;
}
if (selectedActivityEndMarker) {
selectedActivityEndMarker.remove();
selectedActivityEndMarker = null;
}
}
// ===============================
// TOGGLE TUTTE LE TRACCE
// ===============================
async function renderAllActivityRoutes() {
if (!activityRoutesLayer) {
activityRoutesLayer = L.layerGroup().addTo(window.globalMap);
}
activityRoutesLayer.clearLayers();
if (!showAllActivityRoutes) return;
let routesToDraw = cachedRouteData;
if (routesToDraw.length > MAX_GLOBAL_ACTIVITY_ROUTES) {
const bounds = window.globalMap.getBounds();
routesToDraw = routesToDraw.filter((route) =>
bounds.contains(route.start),
);
}
if (routesToDraw.length > MAX_GLOBAL_ACTIVITY_ROUTES) {
console.warn(
`[activity map] Troppe tracce da disegnare: ${routesToDraw.length}. Zooma di più.`,
);
return;
}
const colors = assignSmartRouteColors(routesToDraw);
routesToDraw.forEach((route) => {
const color =
colors.get(route.key) ||
ACTIVITY_ROUTE_COLORS[route.index % ACTIVITY_ROUTE_COLORS.length];
const polyline = L.polyline(route.latlngs, {
color,
weight: 5,
opacity: 0.75,
lineCap: "round",
lineJoin: "round",
});
polyline.on("click", () => {
showSelectedActivityOnGlobalMap(route);
});
activityRoutesLayer.addLayer(polyline);
});
}
// ===============================
// COLORI INTELLIGENTI
// ===============================
function assignSmartRouteColors(routes) {
const cellToColors = new Map();
const result = new Map();
routes.forEach((route, index) => {
const usedNearbyColors = new Set();
route.cells.forEach((cell) => {
const colors = cellToColors.get(cell);
if (!colors) return;
colors.forEach((color) => usedNearbyColors.add(color));
});
const color =
ACTIVITY_ROUTE_COLORS.find((c) => !usedNearbyColors.has(c)) ||
ACTIVITY_ROUTE_COLORS[index % ACTIVITY_ROUTE_COLORS.length];
result.set(route.key, color);
route.cells.forEach((cell) => {
if (!cellToColors.has(cell)) {
cellToColors.set(cell, new Set());
}
cellToColors.get(cell).add(color);
});
});
return result;
}
function getRouteCells(points, precision = 3) {
const cells = new Set();
points.forEach((point) => {
const lat = Number(point.lat);
const lng = Number(point.lng);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
cells.add(`${lat.toFixed(precision)}:${lng.toFixed(precision)}`);
});
return cells;
}
// ===============================
// HELPERS
// ===============================
function isProbablyActivity(item) {
if (!item) return false;
const format = String(
item.format ||
item.file_extension ||
item.mime_type ||
item.file_name ||
item.name ||
"",
).toLowerCase();
return (
format.includes("fit") ||
format.includes("gpx") ||
format.includes("tcx") ||
item.distance_meters != null ||
item.duration_millis != null ||
item.track_preview != null
);
}
async function getRoutePointsSafe(item) {
try {
if (typeof window.getActivityRoutePoints === "function") {
return await window.getActivityRoutePoints(item);
}
if (typeof getActivityRoutePoints === "function") {
return await getActivityRoutePoints(item);
}
} catch (error) {
console.warn("[activity map] errore caricamento punti:", error);
}
return [];
}
function pointsToLatLngs(points) {
return points
.map((point) => [Number(point.lat), Number(point.lng)])
.filter(
(point) => Number.isFinite(point[0]) && Number.isFinite(point[1]),
);
}
function getActivityKey(item, index) {
return String(item.id || item.path || item.file_name || item.name || index);
}
function createActivityPopupHtml(item) {
const key = escapeHtml(
String(item.id || item.path || item.file_name || item.name || ""),
);
const title =
item.display_name ||
item.route_name ||
item.title ||
item.file_name ||
item.name ||
"Attività";
const distance = Number(item.distance_meters);
const duration = Number(item.duration_millis);
const distanceText = Number.isFinite(distance)
? `${(distance / 1000).toFixed(1)} km`
: "-";
const durationText =
Number.isFinite(duration) &&
typeof window.formatDurationShort === "function"
? window.formatDurationShort(duration)
: formatDurationFallback(duration);
return `
<div class="activity-map-popup">
<strong>${escapeHtml(title)}</strong>
${item.sport ? `<div>${escapeHtml(item.sport)}</div>` : ""}
<div>${distanceText} · ${durationText}</div>
<button
type="button"
class="activity-popup-open"
data-open-activity-key="${key}"
>
Apri percorso
</button>
</div>
`;
}
function formatDurationFallback(milliseconds) {
const value = Number(milliseconds);
if (!Number.isFinite(value)) return "-";
const totalSeconds = Math.round(value / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
// ===============================
// GEOJSON MAPBOX ATTIVITÀ
// ===============================
window.getActivityMapGeoJSON = function () {
const features = [];
for (const route of cachedRouteData) {
if (!route.start) {
continue;
}
features.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: [route.start.lng, route.start.lat],
},
properties: {
id: route.key,
type: "activity",
name:
route.item.display_name ||
route.item.route_name ||
route.item.name ||
"Attività",
},
});
}
return features;
};
// ===============================
// LINEE PERCORSI MAPBOX
// ===============================
window.getActivityRoutesGeoJSON = function () {
const features = [];
for (const route of cachedRouteData) {
if (!route.latlngs || route.latlngs.length < 2) {
continue;
}
features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: route.latlngs.map((p) => [p.lng, p.lat]),
},
properties: {
id: route.key,
type: "route",
},
});
}
return features;
};
})();