From dd824708354a90cba5b234e03abc5dd5ab272511 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 6 May 2024 11:36:00 +0300 Subject: [PATCH] Arrange map markers to overlap less --- static/js/map.js | 155 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 126 insertions(+), 29 deletions(-) diff --git a/static/js/map.js b/static/js/map.js index 8403f41..8aaea69 100644 --- a/static/js/map.js +++ b/static/js/map.js @@ -22,39 +22,136 @@ var icons = { "event": emoji_icon("🍷"), } + function build_map(map_id, coordinates, routes) { - // Initialize the map - var map = L.map(map_id).fitBounds(coordinates.map(function(station) { - return [station.latitude, station.longitude]; - })); + var map = L.map(map_id).fitBounds(coordinates.map(station => [station.latitude, station.longitude])); - // Set up the tile layer - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors' - }).addTo(map); + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(map); - // Add markers with appropriate icons to the map - coordinates.forEach(function(item) { - var marker = L.marker([item.latitude, item.longitude], { icon: icons[item.type] }).addTo(map); - marker.bindPopup(item.name); - }); + var markers = []; + var offset_lines = []; - // 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) { - // If route is defined as 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 { - // If route is defined by 'from' and 'to' coordinates - L.polyline([route.from, route.to], style).addTo(map); + function getIconBounds(latlng) { + let iconSize = 30; // Assuming the icon size as a square of 30x30 pixels + 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); } - }); - return map; + 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; }