if (![].at) { Array.prototype.at = function(pos) { return this.slice(pos, pos + 1)[0]; }; } var emojiByType = { "station": "🚉", "airport": "✈️", "ferry_terminal": "🚢", "accommodation": "🏨", "conference": "🖥️", "event": "🍷" }; function getIconMetrics(zoom) { var outerSize; if (zoom <= 3) { outerSize = 20; } else if (zoom <= 5) { outerSize = 26; } else if (zoom <= 8) { outerSize = 32; } else { outerSize = 38; } var innerSize = outerSize - 6; var fontSize = Math.max(12, Math.round(innerSize * 0.65)); return { outerSize: outerSize, fontSize: fontSize, anchor: [outerSize / 2, outerSize / 2] }; } function emojiIcon(emoji, zoom) { var symbol = emoji || "📍"; var metrics = getIconMetrics(zoom); var iconStyle = [ "
", "
", symbol, "
" ].join(""); return L.divIcon({ className: "custom-div-icon", html: iconStyle, iconSize: [metrics.outerSize, metrics.outerSize], iconAnchor: metrics.anchor }); } function build_map(map_id, coordinates, routes) { var map = L.map(map_id).fitBounds(coordinates.map(function(station) { return [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, zoom) { var iconSize = getIconMetrics(zoom).outerSize; if (!latlng) return null; var pixel = map.project(latlng, zoom); var sw = map.unproject([pixel.x - iconSize / 2, pixel.y + iconSize / 2], zoom); var ne = map.unproject([pixel.x + iconSize / 2, pixel.y - iconSize / 2], zoom); return L.latLngBounds(sw, ne); } function calculateCentroid(markers) { var latSum = 0, lngSum = 0, count = 0; markers.forEach(function(marker) { latSum += marker.getLatLng().lat; lngSum += marker.getLatLng().lng; count += 1; }); return count > 0 ? L.latLng(latSum / count, lngSum / count) : null; } // Function to detect and group overlapping markers function getOverlappingGroups(zoom) { var groups = []; var visited = new Set(); markers.forEach(function(marker) { if (visited.has(marker)) { return; } var group = []; var markerBounds = getIconBounds(marker.getLatLng(), zoom); markers.forEach(function(otherMarker) { var otherBounds = getIconBounds(otherMarker.getLatLng(), zoom); if (marker !== otherMarker && markerBounds && otherBounds && markerBounds.intersects(otherBounds)) { 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) { var markerPixelSize = Math.max(18, getIconMetrics(zoom).outerSize); var mapRef = group[0]._map; // Assuming all markers are on the same map var centroid = calculateCentroid(group); if (!centroid) { return; } var centroidPoint = mapRef.project(centroid, zoom); var radius = markerPixelSize * 1.1; var angleIncrement = (2 * Math.PI) / group.length; group.forEach(function(marker, index) { var angle = index * angleIncrement; var newX = centroidPoint.x + radius * Math.cos(angle); var newY = centroidPoint.y + radius * Math.sin(angle); var newPoint = L.point(newX, newY); var newLatLng = mapRef.unproject(newPoint, zoom); var originalPos = marker.originalLatLng; marker.setLatLng(newLatLng); marker.polyline = L.polyline([originalPos, newLatLng], {color: "#909090", weight: 1, dashArray: "2,4"}).addTo(mapRef); offset_lines.push(marker.polyline); }); } function updateMarkerIcons(zoom) { markers.forEach(function(marker) { marker.setIcon(emojiIcon(marker.emoji, zoom)); }); } coordinates.forEach(function(item) { var latlng = L.latLng(item.latitude, item.longitude); var marker = L.marker(latlng, { icon: emojiIcon(emojiByType[item.type], map.getZoom()) }).addTo(map); marker.bindPopup(item.name); marker.originalLatLng = latlng; marker.emoji = emojiByType[item.type]; markers.push(marker); }); function resetMarkerPositions() { markers.forEach(function(marker) { marker.setLatLng(marker.originalLatLng); if (marker.polyline) { map.removeLayer(marker.polyline); marker.polyline = null; } }); offset_lines.forEach(function(polyline) { map.removeLayer(polyline); }); offset_lines = []; } map.on('zoomend', function() { resetMarkerPositions(); updateMarkerIcons(map.getZoom()); var overlappingGroups = getOverlappingGroups(map.getZoom()); overlappingGroups.forEach(function(group) { return displaceMarkers(group, map.getZoom()); }); }); updateMarkerIcons(map.getZoom()); var initialGroups = getOverlappingGroups(map.getZoom()); initialGroups.forEach(function(group) { return displaceMarkers(group, map.getZoom()); }); // Draw routes routes.forEach(function(route) { var color = {"train": "blue", "flight": "red", "unbooked_flight": "orange", "coach": "green"}[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); } }); var mapElement = document.getElementById(map_id); document.getElementById('toggleMapSize').addEventListener('click', function() { var mapElement = document.getElementById(map_id); var isFullWindow = mapElement.classList.contains('full-window-map'); if (isFullWindow) { mapElement.classList.remove('full-window-map'); mapElement.classList.add('half-map'); mapElement.style.position = 'relative'; } else { mapElement.classList.add('full-window-map'); mapElement.classList.remove('half-map'); mapElement.style.position = ''; } // Ensure the map adjusts to the new container size map.invalidateSize(); }); return map; }