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

964 lines
16 KiB
JavaScript

// public/js/map/activities/activitySpiderfy.js
// MAPBOX ACTIVITY SPIDERFY
(() => {
"use strict";
let mapboxMap = null;
let eventsMap = null;
const spiderMarkers = [];
let spiderfyOpen = false;
const SOURCE_ID =
"activities";
const CLUSTER_LAYER_ID =
"activities-clusters";
// ===================================
// SET MAP
// ===================================
function setMap(map) {
if (!map) {
return;
}
if (eventsMap === map) {
mapboxMap = map;
return;
}
detachEvents();
mapboxMap = map;
eventsMap = map;
eventsMap.on(
"zoomstart",
handleZoomStart,
);
eventsMap.on(
"moveend",
handleMoveEnd,
);
eventsMap.on(
"click",
handleMapClick,
);
console.log(
"[ActivitySpiderfy] setMap",
);
}
// ===================================
// REMOVE EVENTS
// ===================================
function detachEvents() {
if (!eventsMap) {
return;
}
eventsMap.off(
"zoomstart",
handleZoomStart,
);
eventsMap.off(
"moveend",
handleMoveEnd,
);
eventsMap.off(
"click",
handleMapClick,
);
eventsMap = null;
}
// ===================================
// MAP EVENTS
// ===================================
function handleZoomStart() {
if (!spiderfyOpen) {
return;
}
clear();
}
function handleMoveEnd() {
if (spiderfyOpen) {
return;
}
window.MapboxActivityClusters
?.update?.();
const activities =
getAllActivities();
window.MapboxActivityMarkers
?.update?.(
activities,
);
}
function handleMapClick(event) {
if (!spiderfyOpen) {
return;
}
if (
!mapboxMap?.getLayer?.(
CLUSTER_LAYER_ID,
)
) {
clear();
return;
}
const hits =
mapboxMap.queryRenderedFeatures(
event.point,
{
layers: [
CLUSTER_LAYER_ID,
],
},
);
if (hits.length > 0) {
return;
}
clear();
}
// ===================================
// GET ACTIVITIES
// ===================================
function getAllActivities() {
const controllerActivities =
window.MapboxMarkers
?.getActivities?.() || [];
if (controllerActivities.length) {
return controllerActivities;
}
if (!mapboxMap) {
return [];
}
if (
!mapboxMap.getSource(
SOURCE_ID,
)
) {
return [];
}
return mapboxMap.querySourceFeatures(
SOURCE_ID,
{
filter: [
"!",
["has", "point_count"],
],
},
);
}
// ===================================
// DISTANCE
// ===================================
function distanceMeters(a, b) {
if (
!Array.isArray(a) ||
!Array.isArray(b) ||
a.length < 2 ||
b.length < 2
) {
return Infinity;
}
const R = 6371000;
const lat1 =
Number(a[1]) *
Math.PI /
180;
const lat2 =
Number(b[1]) *
Math.PI /
180;
const dLat =
lat2 - lat1;
const dLng =
(
Number(b[0]) -
Number(a[0])
) *
Math.PI /
180;
const h =
Math.sin(dLat / 2) ** 2 +
Math.cos(lat1) *
Math.cos(lat2) *
Math.sin(dLng / 2) ** 2;
return (
2 *
R *
Math.atan2(
Math.sqrt(h),
Math.sqrt(1 - h),
)
);
}
// ===================================
// NEARBY ACTIVITIES
// ===================================
function getNearbyActivities(
feature,
maxDistance = 3,
) {
const center =
feature?.geometry?.coordinates;
if (
!Array.isArray(center) ||
center.length < 2
) {
return [];
}
return getAllActivities().filter(
(other) => {
const coordinates =
other?.geometry?.coordinates;
return (
distanceMeters(
center,
coordinates,
) <= maxDistance
);
},
);
}
// ===================================
// ACTIVITY ITEM
// ===================================
function isActivityItem(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
);
}
function getActivityKey(
item,
index,
) {
return String(
item?.id ||
item?.path ||
item?.file_name ||
item?.name ||
index,
);
}
function openActivityFeature(
feature,
) {
const id =
String(
feature?.properties?.id ??
"",
);
if (!id) {
return;
}
const allItems =
typeof window.getLocalItems ===
"function"
? window.getLocalItems()
: [];
const activityItems =
allItems.filter(
isActivityItem,
);
const itemIndex =
activityItems.findIndex(
(item, index) =>
getActivityKey(
item,
index,
) === id,
);
if (itemIndex < 0) {
console.warn(
"[ActivitySpiderfy] attività non trovata",
{
id,
available:
activityItems.length,
},
);
return;
}
if (
typeof window.openActivityModal !==
"function"
) {
return;
}
window.openActivityModal(
activityItems[itemIndex],
activityItems,
itemIndex,
);
}
// ===================================
// ICON
// ===================================
function getActivityIcon(
activityName,
) {
const sport = String(
activityName || "",
)
.toLowerCase()
.trim();
if (
sport.includes("cycling") ||
sport.includes("cycle") ||
sport.includes("ride") ||
sport.includes("bike") ||
sport.includes("bici") ||
sport.includes("cicl")
) {
return "🚴";
}
if (
sport.includes("running") ||
sport.includes("run") ||
sport.includes("corsa")
) {
return "🏃";
}
if (
sport.includes("walking") ||
sport.includes("walk") ||
sport.includes("cammin")
) {
return "🚶";
}
if (
sport.includes("hiking") ||
sport.includes("hike") ||
sport.includes("trek")
) {
return "🥾";
}
if (
sport.includes("swimming") ||
sport.includes("swim") ||
sport.includes("nuoto")
) {
return "🏊";
}
return "📍";
}
// ===================================
// SPIDER ELEMENT
// ===================================
function createSpiderElement(
feature,
) {
const el =
document.createElement("div");
el.className =
"activity-marker activity-spider-marker";
const inner =
document.createElement("div");
inner.className =
"activity-marker-inner";
const icon =
document.createElement("span");
icon.className =
"activity-marker-icon";
icon.textContent =
getActivityIcon(
feature?.properties?.sport,
);
inner.appendChild(icon);
el.appendChild(inner);
return el;
}
// ===================================
// SPIDER MARKER
// ===================================
function createSpiderMarker(
feature,
id,
) {
if (!mapboxMap) {
return null;
}
const coordinates =
feature?.geometry?.coordinates;
if (
!Array.isArray(coordinates) ||
coordinates.length < 2
) {
return null;
}
const el =
createSpiderElement(
feature,
);
el.addEventListener(
"pointerdown",
(event) => {
event.stopPropagation();
},
);
el.addEventListener(
"click",
(event) => {
event.preventDefault();
event.stopPropagation();
openActivityFeature(
feature,
);
clear();
},
);
const marker =
new mapboxgl.Marker({
element: el,
anchor: "center",
})
.setLngLat(coordinates)
.addTo(mapboxMap);
marker.__originalLngLat =
coordinates;
marker.__activityId =
id;
marker.__spider =
true;
return marker;
}
// ===================================
// HIDE / SHOW CLUSTERS
// ===================================
function hideClusterMarkers() {
window.MapboxActivityClusters
?.hide?.();
}
function showClusterMarkers() {
window.MapboxActivityClusters
?.show?.();
}
// ===================================
// CLEAR
// ===================================
function clear() {
for (
const marker
of spiderMarkers
) {
marker.remove();
}
spiderMarkers.length = 0;
spiderfyOpen = false;
if (mapboxMap) {
mapboxMap.triggerRepaint();
}
showClusterMarkers();
window.MapboxActivityClusters
?.update?.();
window.MapboxActivityMarkers
?.update?.(
getAllActivities(),
);
console.log(
"[ActivitySpiderfy] chiuso",
);
}
// ===================================
// SPIDERFY
// ===================================
function spiderfyActivities(
features,
) {
if (
!mapboxMap ||
!Array.isArray(features) ||
features.length === 0
) {
return;
}
clear();
const center =
features[0]
?.geometry
?.coordinates;
if (
!Array.isArray(center) ||
center.length < 2
) {
return;
}
const count =
features.length;
const radiusPx =
Math.min(
96,
Math.max(
30,
28 + count * 6,
),
);
const centerPoint =
mapboxMap.project(
center,
);
hideClusterMarkers();
features.forEach(
(feature, index) => {
const rawId =
feature?.properties?.id;
if (
rawId === undefined ||
rawId === null
) {
return;
}
const id =
String(rawId);
window.MapboxActivityMarkers
?.remove?.(
id,
);
const marker =
createSpiderMarker(
feature,
id,
);
if (!marker) {
return;
}
const angle =
(
Math.PI *
2 *
index
) /
count -
Math.PI / 2;
const point = {
x:
centerPoint.x +
Math.cos(angle) *
radiusPx,
y:
centerPoint.y +
Math.sin(angle) *
radiusPx,
};
const lngLat =
mapboxMap.unproject(
point,
);
marker.setLngLat(
lngLat,
);
spiderMarkers.push(
marker,
);
},
);
spiderfyOpen =
spiderMarkers.length > 0;
console.log(
"[ActivitySpiderfy] aperto",
{
activities:
spiderMarkers.length,
radiusPx,
},
);
}
// ===================================
// SMART ZOOM
// ===================================
function smartZoomCluster(
clusterId,
center,
) {
if (!mapboxMap) {
return;
}
const source =
mapboxMap.getSource(
SOURCE_ID,
);
if (!source) {
return;
}
const MAX_STEPS = 5;
let steps = 0;
function zoomStep() {
if (steps >= MAX_STEPS) {
return;
}
steps++;
source.getClusterExpansionZoom(
Number(clusterId),
(
error,
expansionZoom,
) => {
if (error) {
console.error(
"[ActivitySpiderfy] expansion zoom error",
error,
);
return;
}
const currentZoom =
mapboxMap.getZoom();
const targetZoom =
Math.max(
expansionZoom,
currentZoom + 1,
);
mapboxMap.once(
"moveend",
() => {
const clusters =
mapboxMap
.querySourceFeatures(
SOURCE_ID,
{
filter: [
"has",
"point_count",
],
},
);
const sameCluster =
clusters.some(
(feature) =>
Number(
feature
?.properties
?.cluster_id,
) ===
Number(clusterId),
);
if (sameCluster) {
zoomStep();
} else {
window
.MapboxActivityClusters
?.update?.();
window
.MapboxActivityMarkers
?.update?.(
getAllActivities(),
);
}
},
);
mapboxMap.easeTo({
center,
zoom: targetZoom,
duration: 700,
});
},
);
}
zoomStep();
}
// ===================================
// CLUSTER CLICK
// ===================================
function handleClusterClick(
clusterId,
center,
) {
if (!mapboxMap) {
return;
}
const source =
mapboxMap.getSource(
SOURCE_ID,
);
if (!source) {
return;
}
source.getClusterLeaves(
Number(clusterId),
100,
0,
(error, leaves) => {
if (error) {
console.error(
"[ActivitySpiderfy] cluster leaves error",
error,
);
return;
}
if (
!Array.isArray(leaves) ||
leaves.length === 0
) {
return;
}
const firstCoordinates =
leaves[0]
?.geometry
?.coordinates;
if (
!Array.isArray(
firstCoordinates,
)
) {
return;
}
const radiusMeters = 30;
const allNearby =
leaves.every(
(feature) =>
distanceMeters(
firstCoordinates,
feature
?.geometry
?.coordinates,
) <= radiusMeters,
);
console.log(
"[ActivitySpiderfy] cluster click",
{
clusterId,
leaves:
leaves.length,
spider:
allNearby,
},
);
if (allNearby) {
spiderfyActivities(
leaves,
);
return;
}
smartZoomCluster(
clusterId,
center,
);
},
);
}
// ===================================
// STATE
// ===================================
function isOpen() {
return spiderfyOpen;
}
function size() {
return spiderMarkers.length;
}
function debug() {
const result = {
open:
spiderfyOpen,
markers:
spiderMarkers.length,
map:
!!mapboxMap,
};
console.log(
"[ActivitySpiderfy] debug",
result,
);
return result;
}
// ===================================
// PUBLIC API
// ===================================
window.MapboxActivitySpiderfy = {
setMap,
clear,
spiderfyActivities,
getNearbyActivities,
handleClusterClick,
smartZoomCluster,
isOpen,
size,
debug,
};
})();