Arrange map markers to overlap less

This commit is contained in:
Edward Betts 2024-05-06 11:36:00 +03:00
parent c65e60a1f1
commit dd82470835

View file

@ -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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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;
}