add spot clustering
This commit is contained in:
parent
d06a78d077
commit
761eb66d1f
4 changed files with 185 additions and 21 deletions
|
|
@ -36,3 +36,81 @@ body.home-page main > div {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
height: 100%;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
106
assets/js/app.js
106
assets/js/app.js
|
|
@ -40,6 +40,52 @@ Hooks.APRSMap = {
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
}).addTo(map);
|
}).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: "<div><span>" + childCount + "</span></div>",
|
||||||
|
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
|
// Store markers to avoid duplicates
|
||||||
this.markers = new Map();
|
this.markers = new Map();
|
||||||
this.packetCount = 0;
|
this.packetCount = 0;
|
||||||
|
|
@ -88,10 +134,15 @@ Hooks.APRSMap = {
|
||||||
},
|
},
|
||||||
|
|
||||||
clearAllMarkers() {
|
clearAllMarkers() {
|
||||||
// Remove all markers from the map
|
// Remove all markers from the cluster group or map
|
||||||
this.markers.forEach((marker) => {
|
if (this.markerClusterGroup) {
|
||||||
this.map.removeLayer(marker);
|
this.markerClusterGroup.clearLayers();
|
||||||
});
|
} else {
|
||||||
|
// Fallback: remove markers directly from map
|
||||||
|
this.markers.forEach((marker) => {
|
||||||
|
this.map.removeLayer(marker);
|
||||||
|
});
|
||||||
|
}
|
||||||
this.markers.clear();
|
this.markers.clear();
|
||||||
this.packetCount = 0;
|
this.packetCount = 0;
|
||||||
const counterElement = document.getElementById("packet-count");
|
const counterElement = document.getElementById("packet-count");
|
||||||
|
|
@ -101,31 +152,25 @@ Hooks.APRSMap = {
|
||||||
},
|
},
|
||||||
|
|
||||||
removeMarkersOutsideBounds(bounds) {
|
removeMarkersOutsideBounds(bounds) {
|
||||||
// Remove markers that are outside the current bounds
|
// With clustering, the cluster group handles visibility automatically
|
||||||
const markersToRemove = [];
|
// We'll just track the count of markers within bounds
|
||||||
|
let visibleCount = 0;
|
||||||
|
|
||||||
this.markers.forEach((marker, callsign) => {
|
this.markers.forEach((marker, callsign) => {
|
||||||
const latLng = marker.getLatLng();
|
const latLng = marker.getLatLng();
|
||||||
if (!bounds.contains(latLng)) {
|
if (bounds.contains(latLng)) {
|
||||||
this.map.removeLayer(marker);
|
visibleCount++;
|
||||||
markersToRemove.push(callsign);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove from our tracking map
|
// Update counter to show visible markers
|
||||||
markersToRemove.forEach((callsign) => {
|
|
||||||
this.markers.delete(callsign);
|
|
||||||
this.packetCount--;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update counter
|
|
||||||
const counterElement = document.getElementById("packet-count");
|
const counterElement = document.getElementById("packet-count");
|
||||||
if (counterElement) {
|
if (counterElement) {
|
||||||
counterElement.textContent = this.packetCount;
|
counterElement.textContent = visibleCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify server of the updated packet count
|
// Notify server of the updated packet count
|
||||||
this.pushEvent("update_packet_count", { count: this.packetCount });
|
this.pushEvent("update_packet_count", { count: visibleCount });
|
||||||
},
|
},
|
||||||
|
|
||||||
addPacketMarker(packet) {
|
addPacketMarker(packet) {
|
||||||
|
|
@ -168,8 +213,17 @@ Hooks.APRSMap = {
|
||||||
if (this.markers.has(callsign)) {
|
if (this.markers.has(callsign)) {
|
||||||
// Update existing marker
|
// Update existing marker
|
||||||
const existingMarker = this.markers.get(callsign);
|
const existingMarker = this.markers.get(callsign);
|
||||||
existingMarker.setLatLng([lat, lng]);
|
if (this.markerClusterGroup) {
|
||||||
existingMarker.setPopupContent(popupContent);
|
// 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 {
|
} else {
|
||||||
// Create new marker
|
// Create new marker
|
||||||
const icon = this.createAPRSIcon(
|
const icon = this.createAPRSIcon(
|
||||||
|
|
@ -177,7 +231,14 @@ Hooks.APRSMap = {
|
||||||
packet["data_extended"]["symbol_code"] || ">",
|
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);
|
this.markers.set(callsign, marker);
|
||||||
console.log("New marker added for:", callsign, "Total markers:", this.markers.size);
|
console.log("New marker added for:", callsign, "Total markers:", this.markers.size);
|
||||||
|
|
@ -206,6 +267,9 @@ Hooks.APRSMap = {
|
||||||
|
|
||||||
destroyed() {
|
destroyed() {
|
||||||
console.log("APRSMap hook destroyed");
|
console.log("APRSMap hook destroyed");
|
||||||
|
if (this.markerClusterGroup) {
|
||||||
|
this.markerClusterGroup.clearLayers();
|
||||||
|
}
|
||||||
if (this.map) {
|
if (this.map) {
|
||||||
console.log("Removing map instance");
|
console.log("Removing map instance");
|
||||||
this.map.remove();
|
this.map.remove();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,14 @@
|
||||||
integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
|
integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
|
||||||
crossorigin=""
|
crossorigin=""
|
||||||
/>
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"
|
||||||
|
/>
|
||||||
<!-- Make sure you put this AFTER Leaflet's CSS -->
|
<!-- Make sure you put this AFTER Leaflet's CSS -->
|
||||||
<script
|
<script
|
||||||
src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
|
src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
|
||||||
|
|
@ -11,5 +19,9 @@
|
||||||
crossorigin=""
|
crossorigin=""
|
||||||
>
|
>
|
||||||
</script>
|
</script>
|
||||||
|
<script
|
||||||
|
src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"
|
||||||
|
>
|
||||||
|
</script>
|
||||||
|
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
|
|
|
||||||
|
|
@ -118,12 +118,22 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||||
crossorigin=""
|
crossorigin=""
|
||||||
/>
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"
|
||||||
|
/>
|
||||||
<script
|
<script
|
||||||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||||
crossorigin=""
|
crossorigin=""
|
||||||
>
|
>
|
||||||
</script>
|
</script>
|
||||||
|
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#aprs-map {
|
#aprs-map {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue