galleria_photo_face_rknn_op.../public/js/map/activitiesLayer.js
2026-10-03 12:16:58 +02:00

1087 lines
24 KiB
JavaScript

// public/js/map/activitiesLayer.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;
let activityStartMarkers = [];
// ===============================
// CLUSTER ATTIVITÀ
// ===============================
window.createActivityClusterIcon = function (cluster) {
const count = cluster.getChildCount();
const cls =
count > 200
? "cluster-xl"
: count > 50
? "cluster-lg"
: count > 10
? "cluster-md"
: "cluster-sm";
return L.divIcon({
html: `
<div class="leaflet-activity-cluster ${cls}">
<span>${count}</span>
</div>
`,
className: "activity-leaflet-cluster",
iconSize: L.point(48, 48),
iconAnchor: L.point(24, 24),
});
};
// ===============================
// PULIZIA LAYER ATTIVITÀ
// ===============================
window.clearActivityMapLayers = function () {
clearActivityStartMarkers();
if (activityRoutesLayer) {
activityRoutesLayer.clearLayers();
}
if (selectedActivityLayer) {
selectedActivityLayer.remove();
selectedActivityLayer = null;
}
if (selectedActivityEndMarker) {
selectedActivityEndMarker.remove();
selectedActivityEndMarker = null;
}
};
// ===============================
// PULIZIA MARKERS
// ===============================
function clearActivityStartMarkers() {
const markersToRemove = [...activityStartMarkers];
activityStartMarkers.length = 0;
for (const marker of markersToRemove) {
try {
window.globalMarkers?.removeLayer?.(marker);
/*
* Rimuove direttamente il marker
* dalla mappa Leaflet, se presente.
*/
if (window.globalMap?.hasLayer?.(marker)) {
window.globalMap.removeLayer(marker);
}
marker.remove?.();
} catch (error) {
console.warn(
"[activitiesLayer] errore rimozione marker Leaflet",
error,
);
}
}
}
// ===============================
// TOGGLE TRACCE
// ===============================
function ensureActivityRoutesToggle() {
let btn = document.getElementById("toggleActivityRoutesBtn");
if (!btn) {
btn = document.createElement("button");
btn.id = "toggleActivityRoutesBtn";
btn.type = "button";
btn.className = "map-toggle-routes";
btn.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
showAllActivityRoutes = !showAllActivityRoutes;
updateActivityRoutesToggle();
const isMapbox3D = !!window.MapTerrain3D?.enabled?.();
if (isMapbox3D) {
updateMapboxActivityRoutes();
} else {
await renderAllActivityRoutes();
}
});
}
moveActivityRoutesToggle(btn);
updateActivityRoutesToggle();
return btn;
}
function moveActivityRoutesToggle(btn) {
if (!btn) {
return;
}
const isMapbox3D = !!window.MapTerrain3D?.enabled?.();
const targetContainer = isMapbox3D
? document.getElementById("terrainMap")
: document.getElementById("globalMap");
if (!targetContainer) {
return;
}
if (btn.parentElement !== targetContainer) {
targetContainer.appendChild(btn);
}
}
function updateActivityRoutesToggle() {
const btn = document.getElementById("toggleActivityRoutesBtn");
if (!btn) {
return;
}
btn.classList.toggle("active", showAllActivityRoutes);
btn.textContent = showAllActivityRoutes
? "Nascondi tracce"
: "Mostra tracce";
btn.setAttribute("aria-pressed", showAllActivityRoutes ? "true" : "false");
btn.setAttribute(
"aria-label",
showAllActivityRoutes
? "Nascondi tutte le tracce"
: "Mostra tutte le tracce",
);
btn.title = showAllActivityRoutes
? "Nascondi tutte le tracce"
: "Mostra tutte le tracce";
}
window.hideActivityRoutesToggle =
function (resetState = false) {
const btn =
document.getElementById(
"toggleActivityRoutesBtn",
);
if (btn) {
btn.style.display =
"none";
btn.style.visibility =
"hidden";
btn.style.pointerEvents =
"none";
}
if (!resetState) {
return;
}
showAllActivityRoutes =
false;
updateActivityRoutesToggle();
if (
window.MapTerrain3D
?.enabled?.()
) {
window.MapboxMarkers
?.setRoutes?.([]);
}
activityRoutesLayer
?.clearLayers?.();
};
function showActivityRoutesToggle() {
const btn =
ensureActivityRoutesToggle();
if (!btn) {
return;
}
moveActivityRoutesToggle(
btn,
);
btn.style.display =
"inline-flex";
btn.style.visibility =
"visible";
btn.style.pointerEvents =
"auto";
updateActivityRoutesToggle();
}
window.showActivityRoutesToggle =
showActivityRoutesToggle;
// ===============================
// ICONA MARKER ATTIVITÀ
// ===============================
function createActivityStartIcon(item) {
const sport = String(item?.sport || "")
.toLowerCase()
.trim();
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") ||
sport.includes("cycling") ||
sport.includes("ride") ||
sport.includes("cicl")
) {
emoji = "🚴";
cls = "activity-marker-bike";
} else if (sport.includes("walk") || sport.includes("cammin")) {
emoji = "🚶";
cls = "activity-marker-walk";
} else if (
sport.includes("hike") ||
sport.includes("hiking") ||
sport.includes("trek")
) {
emoji = "🥾";
cls = "activity-marker-hiking";
} else if (sport.includes("swim") || sport.includes("nuoto")) {
emoji = "🏊";
cls = "activity-marker-swimming";
}
return L.divIcon({
html: `
<div class="activity-start-marker ${cls}">
<span class="activity-start-marker-icon">
${emoji}
</span>
</div>
`,
className: "activity-leaflet-marker",
iconSize: L.point(42, 42),
iconAnchor: L.point(21, 21),
});
}
// ===============================
// RENDER ATTIVITÀ
// ===============================
window.renderActivityMapLayer =
async function () {
if (
window.state?.mode !==
"activities"
) {
return;
}
const renderMode =
window.state.mode;
const isMapbox3D =
!!window.MapTerrain3D
?.enabled?.();
if (
!isMapbox3D &&
(
!window.globalMap ||
!window.globalMarkers
)
) {
return;
}
const token =
++lastRenderToken;
clearActivityStartMarkers();
showActivityRoutesToggle();
/*
* Il pulsante e i layer Leaflet vengono
* gestiti soltanto in modalità standard.
*/
if (!isMapbox3D) {
if (!activityRoutesLayer) {
activityRoutesLayer = L.layerGroup().addTo(window.globalMap);
}
activityRoutesLayer.clearLayers();
clearSelectedActivityOnly();
} else {
/*
* Durante la transizione al 3D pulisce
* eventuali residui senza richiedere
* che Leaflet sia ancora disponibile.
*/
activityRoutesLayer?.clearLayers?.();
clearSelectedActivityOnly();
}
// =================================
// RECUPERA LE ATTIVITÀ
// =================================
const items =
typeof window.getLocalItems === "function" ? window.getLocalItems() : [];
const activityItems = items.filter(isProbablyActivity);
cachedRouteData = [];
// =================================
// CARICA I PUNTI DEI PERCORSI
// =================================
const routeDataList = await Promise.all(
activityItems.map(async (item, index) => {
const points = await getRoutePointsSafe(item);
if (!Array.isArray(points) || points.length < 2) {
return null;
}
const latlngs = pointsToLatLngs(points);
if (!Array.isArray(latlngs) || 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),
};
}),
);
/*
* Se nel frattempo è iniziato un altro
* rendering, questo risultato è obsoleto.
*/
if (
token !== lastRenderToken ||
window.state?.mode !==
renderMode
) {
return;
}
cachedRouteData = routeDataList.filter(Boolean);
console.log("[activitiesLayer] route caricate", {
inputItems: items.length,
activityItems: activityItems.length,
routeResults: routeDataList.length,
validRoutes: cachedRouteData.length,
isMapbox3D,
});
// =================================
// MODALITÀ MAPBOX 3D
// =================================
if (isMapbox3D) {
activityRoutesLayer?.clearLayers?.();
clearSelectedActivityOnly();
const activityFeatures =
typeof window.getActivityMapGeoJSON === "function"
? window.getActivityMapGeoJSON()
: [];
const routeFeatures =
typeof window.getActivityRoutesGeoJSON === "function"
? window.getActivityRoutesGeoJSON()
: [];
console.log("[activitiesLayer] invio dati a Mapbox", {
cachedActivities: cachedRouteData.length,
activityFeatures: activityFeatures.length,
routeFeatures: routeFeatures.length,
});
if (typeof window.MapboxMarkers?.setActivities === "function") {
window.MapboxMarkers.setActivities(activityFeatures);
} else {
console.warn(
"[activitiesLayer] MapboxMarkers.setActivities non disponibile",
);
}
if (typeof window.MapboxMarkers?.setRoutes === "function") {
window.MapboxMarkers.setRoutes(
showAllActivityRoutes ? routeFeatures : [],
);
}
} else {
// =================================
// MODALITÀ LEAFLET
// =================================
console.log("[activitiesLayer] rendering marker Leaflet", {
routes: cachedRouteData.length,
globalMap: !!window.globalMap,
globalMarkers: !!window.globalMarkers,
mapEngineType: window.MapEngine?.type,
mapEngineMapIsLeaflet: window.MapEngine?.map === window.globalMap,
markerLayerIsGlobalMarkers:
window.MapEngine?.markerLayer === window.globalMarkers,
});
if (
window.globalMap &&
window.globalMarkers &&
!window.globalMap.hasLayer(window.globalMarkers)
) {
console.warn(
"[activitiesLayer] cluster group non presente, lo ripristino",
);
window.globalMarkers.addTo(window.globalMap);
}
for (const route of cachedRouteData) {
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",
(event) => {
event?.originalEvent
?.preventDefault?.();
event?.originalEvent
?.stopPropagation?.();
const activityList =
cachedRouteData.map(
(routeData) =>
routeData.item,
);
const activityIndex =
cachedRouteData.findIndex(
(routeData) =>
routeData.key ===
route.key,
);
window.openActivityModal?.(
route.item,
activityList,
activityIndex >= 0
? activityIndex
: 0,
);
},
);
if (window.globalMarkers?.addLayer) {
window.globalMarkers.addLayer(marker);
} else {
console.warn("[activitiesLayer] globalMarkers non disponibile", {
globalMarkers: window.globalMarkers,
mapEngineType: window.MapEngine?.type,
markerLayer: window.MapEngine?.markerLayer,
});
}
activityStartMarkers.push(marker);
}
console.log("[activitiesLayer] marker Leaflet aggiunti", {
trackedMarkers: activityStartMarkers.length,
clusterLayers: window.globalMarkers?.getLayers?.()?.length ?? null,
clusterOnMap: !!(
window.globalMap &&
window.globalMarkers &&
window.globalMap.hasLayer(window.globalMarkers)
),
mode: window.state?.mode,
});
}
// =================================
// ADATTA LA MAPPA ALLE ATTIVITÀ
// =================================
if (
token !== lastRenderToken ||
window.state?.mode !==
renderMode
) {
return;
}
/*
* fitBounds automatico soltanto nella
* modalità standard.
*
* In Mapbox 3D può provocare un ulteriore
* cambio zoom proprio mentre cluster e
* marker vengono aggiornati.
*/
if (!isMapbox3D && window.globalMap && cachedRouteData.length > 0) {
const bounds = L.latLngBounds(
cachedRouteData.map((route) => route.start),
);
if (bounds.isValid()) {
window.globalMap.fitBounds(bounds, {
padding: [60, 60],
maxZoom: 12,
});
}
}
// =================================
// MOSTRA TUTTE LE TRACCE
// =================================
/*
* Le tracce Leaflet devono essere
* renderizzate soltanto nella modalità
* standard. In 3D vengono gestite dalla
* source routes di Mapbox.
*/
if (showAllActivityRoutes && !isMapbox3D) {
await renderAllActivityRoutes();
}
if (
token !== lastRenderToken ||
window.state?.mode !==
renderMode
) {
return;
}
console.log("[activitiesLayer] render completato", {
mode: isMapbox3D ? "mapbox-3d" : "standard",
activities: cachedRouteData.length,
token,
});
};
// ===============================
// 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;
}
}
function updateMapboxActivityRoutes() {
if (!window.MapTerrain3D?.enabled?.()) {
return;
}
if (typeof window.MapboxMarkers?.setRoutes !== "function") {
console.warn("[activitiesLayer] MapboxMarkers.setRoutes non disponibile");
return;
}
if (!showAllActivityRoutes) {
window.MapboxMarkers.setRoutes([]);
console.log("[activitiesLayer] tracce Mapbox nascoste");
return;
}
const routeFeatures =
typeof window.getActivityRoutesGeoJSON === "function"
? window.getActivityRoutesGeoJSON()
: [];
window.MapboxMarkers.setRoutes(routeFeatures);
console.log("[activitiesLayer] tracce Mapbox visibili", {
routes: routeFeatures.length,
});
}
// ===============================
// 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) {
const value =
item?.id ?? item?.path ?? item?.file_name ?? item?.name ?? index;
return String(value);
}
window.getActivityMapKey = getActivityKey;
function createActivityPopupHtml(item) {
const key =
escapeHtml(
getActivityKey(
item,
"",
),
);
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("'", "&#39;");
}
// ===============================
// GEOJSON MAPBOX ATTIVITÀ
// ===============================
window.getActivityMapGeoJSON = function () {
const features = [];
for (const route of cachedRouteData) {
if (!route.start) {
continue;
}
features.push({
type: "Feature",
geometry: {
type: "Point",
// route.start = [lat, lng]
coordinates: [
route.start[1], // lng
route.start[0], // lat
],
},
properties: {
id: route.key,
type: "activity",
name:
route.item.display_name ||
route.item.route_name ||
route.item.name ||
"Attività",
sport: route.item.sport,
distance: route.item.distance_meters,
duration: route.item.duration_millis,
},
});
}
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",
// route.latlngs contiene [lat, lng]
coordinates: route.latlngs.map((p) => [
p[1], // lng
p[0], // lat
]),
},
properties: {
id: route.key,
type: "route",
},
});
}
return features;
};
window.areActivityRoutesVisible = function () {
return showAllActivityRoutes;
};
})();