diff --git a/assets/css/app.css b/assets/css/app.css
index 289d6ee..ca5aab7 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -36,3 +36,81 @@ body.home-page main > div {
max-width: none;
height: 100%;
}
+
+/* Marker cluster styles */
+.marker-cluster-small {
+ background-color: rgba(16, 185, 129, 0.8);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+.marker-cluster-small div {
+ background-color: rgba(16, 185, 129, 0.9);
+ box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.3);
+}
+
+.marker-cluster-medium {
+ background-color: rgba(59, 130, 246, 0.8);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+.marker-cluster-medium div {
+ background-color: rgba(59, 130, 246, 0.9);
+ box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.3);
+}
+
+.marker-cluster-large {
+ background-color: rgba(239, 68, 68, 0.8);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+.marker-cluster-large div {
+ background-color: rgba(239, 68, 68, 0.9);
+ box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.3);
+}
+
+/* Custom cluster icon styles */
+.marker-cluster {
+ background-clip: padding-box;
+ border-radius: 50%;
+ border: 2px solid rgba(255, 255, 255, 0.8);
+}
+.marker-cluster div {
+ width: 30px;
+ height: 30px;
+ margin-left: 5px;
+ margin-top: 5px;
+ text-align: center;
+ border-radius: 50%;
+ font:
+ 13px "Helvetica Neue",
+ Arial,
+ Helvetica,
+ sans-serif;
+ color: white;
+ font-weight: bold;
+ line-height: 30px;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
+}
+.marker-cluster span {
+ line-height: 30px;
+}
+
+/* Cluster animations */
+.leaflet-cluster-anim .leaflet-marker-icon,
+.leaflet-cluster-anim .leaflet-marker-shadow {
+ transition:
+ transform 0.3s ease-out,
+ opacity 0.3s ease-out;
+}
+
+/* Hover effect for clusters */
+.marker-cluster:hover {
+ transform: scale(1.1);
+ z-index: 1000 !important;
+}
+
+/* APRS marker styles */
+.aprs-marker {
+ transition: transform 0.2s ease-out;
+}
+.aprs-marker:hover {
+ transform: scale(1.2);
+ z-index: 1000 !important;
+}
diff --git a/assets/js/app.js b/assets/js/app.js
index e4efb23..2e0c6a7 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -40,6 +40,52 @@ Hooks.APRSMap = {
maxZoom: 19,
}).addTo(map);
+ // Check if MarkerCluster plugin is available
+ if (typeof L.markerClusterGroup === "function") {
+ // Create marker cluster group
+ this.markerClusterGroup = L.markerClusterGroup({
+ chunkedLoading: true,
+ maxClusterRadius: function (zoom) {
+ // Adjust cluster radius based on zoom level
+ // Tighter clustering when zoomed out, looser when zoomed in
+ if (zoom <= 5) return 120;
+ if (zoom <= 8) return 80;
+ if (zoom <= 11) return 60;
+ if (zoom <= 14) return 40;
+ return 20;
+ },
+ spiderfyOnMaxZoom: true,
+ showCoverageOnHover: true,
+ zoomToBoundsOnClick: true,
+ removeOutsideVisibleBounds: true,
+ animate: true,
+ animateAddingMarkers: true,
+ disableClusteringAtZoom: 16,
+ iconCreateFunction: function (cluster) {
+ const childCount = cluster.getChildCount();
+ let c = " marker-cluster-";
+ if (childCount < 10) {
+ c += "small";
+ } else if (childCount < 100) {
+ c += "medium";
+ } else {
+ c += "large";
+ }
+ return new L.DivIcon({
+ html: "
" + childCount + "
",
+ className: "marker-cluster" + c,
+ iconSize: new L.Point(40, 40),
+ });
+ },
+ });
+
+ // Add cluster group to map
+ map.addLayer(this.markerClusterGroup);
+ } else {
+ console.warn("Leaflet MarkerCluster plugin not loaded, falling back to regular markers");
+ this.markerClusterGroup = null;
+ }
+
// Store markers to avoid duplicates
this.markers = new Map();
this.packetCount = 0;
@@ -88,10 +134,15 @@ Hooks.APRSMap = {
},
clearAllMarkers() {
- // Remove all markers from the map
- this.markers.forEach((marker) => {
- this.map.removeLayer(marker);
- });
+ // Remove all markers from the cluster group or map
+ if (this.markerClusterGroup) {
+ this.markerClusterGroup.clearLayers();
+ } else {
+ // Fallback: remove markers directly from map
+ this.markers.forEach((marker) => {
+ this.map.removeLayer(marker);
+ });
+ }
this.markers.clear();
this.packetCount = 0;
const counterElement = document.getElementById("packet-count");
@@ -101,31 +152,25 @@ Hooks.APRSMap = {
},
removeMarkersOutsideBounds(bounds) {
- // Remove markers that are outside the current bounds
- const markersToRemove = [];
+ // With clustering, the cluster group handles visibility automatically
+ // We'll just track the count of markers within bounds
+ let visibleCount = 0;
this.markers.forEach((marker, callsign) => {
const latLng = marker.getLatLng();
- if (!bounds.contains(latLng)) {
- this.map.removeLayer(marker);
- markersToRemove.push(callsign);
+ if (bounds.contains(latLng)) {
+ visibleCount++;
}
});
- // Remove from our tracking map
- markersToRemove.forEach((callsign) => {
- this.markers.delete(callsign);
- this.packetCount--;
- });
-
- // Update counter
+ // Update counter to show visible markers
const counterElement = document.getElementById("packet-count");
if (counterElement) {
- counterElement.textContent = this.packetCount;
+ counterElement.textContent = visibleCount;
}
// Notify server of the updated packet count
- this.pushEvent("update_packet_count", { count: this.packetCount });
+ this.pushEvent("update_packet_count", { count: visibleCount });
},
addPacketMarker(packet) {
@@ -168,8 +213,17 @@ Hooks.APRSMap = {
if (this.markers.has(callsign)) {
// Update existing marker
const existingMarker = this.markers.get(callsign);
- existingMarker.setLatLng([lat, lng]);
- existingMarker.setPopupContent(popupContent);
+ if (this.markerClusterGroup) {
+ // Remove and re-add to cluster group
+ this.markerClusterGroup.removeLayer(existingMarker);
+ existingMarker.setLatLng([lat, lng]);
+ existingMarker.setPopupContent(popupContent);
+ this.markerClusterGroup.addLayer(existingMarker);
+ } else {
+ // Fallback: just update position
+ existingMarker.setLatLng([lat, lng]);
+ existingMarker.setPopupContent(popupContent);
+ }
} else {
// Create new marker
const icon = this.createAPRSIcon(
@@ -177,7 +231,14 @@ Hooks.APRSMap = {
packet["data_extended"]["symbol_code"] || ">",
);
- const marker = L.marker([lat, lng], { icon: icon }).addTo(this.map).bindPopup(popupContent);
+ const marker = L.marker([lat, lng], { icon: icon }).bindPopup(popupContent);
+
+ // Add to cluster group or directly to map
+ if (this.markerClusterGroup) {
+ this.markerClusterGroup.addLayer(marker);
+ } else {
+ marker.addTo(this.map);
+ }
this.markers.set(callsign, marker);
console.log("New marker added for:", callsign, "Total markers:", this.markers.size);
@@ -206,6 +267,9 @@ Hooks.APRSMap = {
destroyed() {
console.log("APRSMap hook destroyed");
+ if (this.markerClusterGroup) {
+ this.markerClusterGroup.clearLayers();
+ }
if (this.map) {
console.log("Removing map instance");
this.map.remove();
diff --git a/lib/aprs_web/controllers/page_html/map.html.heex b/lib/aprs_web/controllers/page_html/map.html.heex
index 79b973b..29b62f1 100644
--- a/lib/aprs_web/controllers/page_html/map.html.heex
+++ b/lib/aprs_web/controllers/page_html/map.html.heex
@@ -4,6 +4,14 @@
integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
crossorigin=""
/>
+
+
+
diff --git a/lib/aprs_web/live/map_live/index.ex b/lib/aprs_web/live/map_live/index.ex
index aab5817..8abb8c2 100644
--- a/lib/aprs_web/live/map_live/index.ex
+++ b/lib/aprs_web/live/map_live/index.ex
@@ -118,12 +118,22 @@ defmodule AprsWeb.MapLive.Index do
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""
/>
+
+
+