if (![].at) { Array.prototype.at = function(pos) { return this.slice(pos, pos + 1)[0] } } function emoji_icon(emoji) { var iconStyle = "
" + emoji + "
"; return L.divIcon({ className: 'custom-div-icon', html: iconStyle, iconSize: [60, 60], iconAnchor: [15, 15], }); } var icons = { "station": emoji_icon("🚉"), "airport": emoji_icon("✈️"), "ferry_terminal": emoji_icon("🚢"), "accommodation": emoji_icon("🏨"), "conference": emoji_icon("🎤"), "event": emoji_icon("🍷"), } function build_map(map_id, coordinates, routes) { var map = L.map(map_id).fitBounds(coordinates.map(station => [station.latitude, station.longitude])); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map); var markers = []; var offset_lines = []; function getIconBounds(latlng) { let iconSize = 20; // Assuming the icon size as a square if (!latlng) return null; let pixel = map.project(latlng, map.getZoom()); let sw = map.unproject([pixel.x - iconSize / 2, pixel.y + iconSize / 2], map.getZoom()); let ne = map.unproject([pixel.x + iconSize / 2, pixel.y - iconSize / 2], map.getZoom()); return L.latLngBounds(sw, ne); } function calculateCentroid(markers) { let latSum = 0, lngSum = 0, count = 0; markers.forEach(marker => { latSum += marker.getLatLng().lat; lngSum += marker.getLatLng().lng; count++; }); return count > 0 ? L.latLng(latSum / count, lngSum / count) : null; } // Function to detect and group overlapping markers function getOverlappingGroups() { let groups = []; let visited = new Set(); markers.forEach((marker, index) => { if (visited.has(marker)) { return; } let group = []; let markerBounds = getIconBounds(marker.getLatLng()); markers.forEach((otherMarker) => { if (marker !== otherMarker && markerBounds.intersects(getIconBounds(otherMarker.getLatLng()))) { group.push(otherMarker); visited.add(otherMarker); } }); if (group.length > 0) { group.push(marker); // Add the original marker to the group groups.push(group); visited.add(marker); } }); return groups; } function displaceMarkers(group, zoom) { const markerPixelSize = 30; // Width/height of the marker in pixels let map = group[0]._map; // Assuming all markers are on the same map let centroid = calculateCentroid(group); let centroidPoint = map.project(centroid, zoom); const radius = markerPixelSize; // Set radius for even distribution const angleIncrement = (2 * Math.PI) / group.length; // Evenly space markers group.forEach((marker, index) => { let angle = index * angleIncrement; let newX = centroidPoint.x + radius * Math.cos(angle); let newY = centroidPoint.y + radius * Math.sin(angle); let newPoint = L.point(newX, newY); let newLatLng = map.unproject(newPoint, zoom); // Store original position for polyline let originalPos = marker.getLatLng(); marker.setLatLng(newLatLng); marker.polyline = L.polyline([originalPos, newLatLng], {color: "gray", weight: 2}).addTo(map); offset_lines.push(marker.polyline); }); } coordinates.forEach(function(item, index) { let latlng = L.latLng(item.latitude, item.longitude); let marker = L.marker(latlng, { icon: icons[item.type] }).addTo(map); marker.bindPopup(item.name); markers.push(marker); }); map.on('zoomend', function() { markers.forEach((marker, index) => { marker.setLatLng([coordinates[index].latitude, coordinates[index].longitude]); // Reset position on zoom if (marker.polyline) { map.removeLayer(marker.polyline); } }); offset_lines.forEach(polyline => { map.removeLayer(polyline); }); let overlappingGroups = getOverlappingGroups(); // console.log(overlappingGroups); // Process or display groups as needed overlappingGroups.forEach(group => displaceMarkers(group, map.getZoom())); }); let overlappingGroups = getOverlappingGroups(); // console.log(overlappingGroups); // Process or display groups as needed overlappingGroups.forEach(group => displaceMarkers(group, map.getZoom())); // Draw routes routes.forEach(function(route) { var color = {"train": "blue", "flight": "red", "unbooked_flight": "orange"}[route.type]; var style = { weight: 3, opacity: 0.5, color: color }; if (route.geojson) { L.geoJSON(JSON.parse(route.geojson), { style: function(feature) { return style; } }).addTo(map); } else if (route.type === "flight" || route.type === "unbooked_flight") { var flightPath = new L.Geodesic([[route.from, route.to]], style).addTo(map); } else { L.polyline([route.from, route.to], style).addTo(map); } }); return map; }