Add Visualize LH Heatmap
This commit is contained in:
parent
2f49848496
commit
1e30134886
4 changed files with 179 additions and 0 deletions
136
static/app.js
136
static/app.js
|
|
@ -10,6 +10,7 @@
|
|||
}).addTo(map);
|
||||
|
||||
// === State ===
|
||||
var heatmapGlowLayer = L.layerGroup().addTo(map);
|
||||
var markerLayer = L.layerGroup().addTo(map);
|
||||
var routeLayer = null;
|
||||
var routePoints = null; // stored [lat, lng] pairs for re-fetching on band change
|
||||
|
|
@ -20,6 +21,12 @@
|
|||
var pinLatLng = null;
|
||||
var debounceTimer = null;
|
||||
var controller = null;
|
||||
var heatmapSocket = null;
|
||||
var heatmapCounts = {};
|
||||
var heatmapMarkerMap = {};
|
||||
var heatmapGlowMarkers = {};
|
||||
var heatmapMaxCount = 1;
|
||||
var heatmapDecayTimer = null;
|
||||
|
||||
// === DOM ===
|
||||
var band2m = document.getElementById("band-2m");
|
||||
|
|
@ -30,6 +37,7 @@
|
|||
var netOther = document.getElementById("net-other");
|
||||
var showHotspots = document.getElementById("show-hotspots");
|
||||
var showInactive = document.getElementById("show-inactive");
|
||||
var showHeatmap = document.getElementById("show-heatmap");
|
||||
var countEl = document.getElementById("count");
|
||||
var fromInput = document.getElementById("route-from");
|
||||
var toInput = document.getElementById("route-to");
|
||||
|
|
@ -278,6 +286,9 @@
|
|||
// === Display markers ===
|
||||
function displayRepeaters(repeaters) {
|
||||
markerLayer.clearLayers();
|
||||
heatmapMarkerMap = {};
|
||||
heatmapGlowLayer.clearLayers();
|
||||
heatmapGlowMarkers = {};
|
||||
repeaters.forEach(function (r) {
|
||||
var color = r.band === "2m" ? "#2196F3" : "#D32F2F";
|
||||
var marker = L.circleMarker([r.lat, r.lng], {
|
||||
|
|
@ -289,6 +300,10 @@
|
|||
});
|
||||
marker.bindPopup(buildPopup(r), { maxWidth: 280 });
|
||||
markerLayer.addLayer(marker);
|
||||
heatmapMarkerMap[r.id] = marker;
|
||||
if (showHeatmap.checked && heatmapCounts[r.id]) {
|
||||
applyHeatmapGlow(r.id, r.lat, r.lng);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -567,6 +582,122 @@
|
|||
fetchRepeaters();
|
||||
}
|
||||
|
||||
// === Heatmap ===
|
||||
function heatColor(intensity) {
|
||||
var r, g;
|
||||
if (intensity < 0.5) {
|
||||
r = Math.round(255 * (intensity * 2));
|
||||
g = 255;
|
||||
} else {
|
||||
r = 255;
|
||||
g = Math.round(255 * (1 - (intensity - 0.5) * 2));
|
||||
}
|
||||
return "rgb(" + r + "," + g + ",0)";
|
||||
}
|
||||
|
||||
function applyHeatmapGlow(repeaterId, lat, lng) {
|
||||
var count = heatmapCounts[repeaterId] || 0;
|
||||
if (count === 0) return;
|
||||
var intensity = Math.min(count / Math.max(heatmapMaxCount, 1), 1.0);
|
||||
var glowRadius = 8 + intensity * 16;
|
||||
var color = heatColor(intensity);
|
||||
if (heatmapGlowMarkers[repeaterId]) {
|
||||
heatmapGlowLayer.removeLayer(heatmapGlowMarkers[repeaterId]);
|
||||
}
|
||||
var glow = L.circleMarker([lat, lng], {
|
||||
radius: glowRadius,
|
||||
fillColor: color,
|
||||
color: color,
|
||||
weight: 2,
|
||||
fillOpacity: 0.25 + intensity * 0.2,
|
||||
opacity: 0.6 + intensity * 0.4,
|
||||
className: "heatmap-glow",
|
||||
interactive: false,
|
||||
});
|
||||
heatmapGlowLayer.addLayer(glow);
|
||||
heatmapGlowMarkers[repeaterId] = glow;
|
||||
}
|
||||
|
||||
function clearHeatmapVisuals() {
|
||||
heatmapGlowLayer.clearLayers();
|
||||
heatmapGlowMarkers = {};
|
||||
}
|
||||
|
||||
function refreshAllGlows() {
|
||||
clearHeatmapVisuals();
|
||||
for (var id in heatmapCounts) {
|
||||
if (heatmapMarkerMap[id]) {
|
||||
var latlng = heatmapMarkerMap[id].getLatLng();
|
||||
applyHeatmapGlow(parseInt(id), latlng.lat, latlng.lng);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function connectHeatmap() {
|
||||
if (heatmapSocket) return;
|
||||
heatmapSocket = io("https://api.brandmeister.network", {
|
||||
path: "/lh/socket.io",
|
||||
transports: ["websocket"],
|
||||
});
|
||||
heatmapSocket.on("connect", function () {
|
||||
console.log("Heatmap: connected to BrandMeister LH");
|
||||
});
|
||||
heatmapSocket.on("disconnect", function () {
|
||||
console.log("Heatmap: disconnected from BrandMeister LH");
|
||||
});
|
||||
heatmapSocket.on("mqtt", function (data) {
|
||||
try {
|
||||
var payload = typeof data.payload === "string"
|
||||
? JSON.parse(data.payload)
|
||||
: data.payload;
|
||||
if (payload.Event !== "Session-Stop") return;
|
||||
var contextId = payload.ContextID;
|
||||
if (!contextId || !heatmapMarkerMap[contextId]) return;
|
||||
heatmapCounts[contextId] = (heatmapCounts[contextId] || 0) + 1;
|
||||
if (heatmapCounts[contextId] > heatmapMaxCount) {
|
||||
heatmapMaxCount = heatmapCounts[contextId];
|
||||
refreshAllGlows();
|
||||
} else {
|
||||
var latlng = heatmapMarkerMap[contextId].getLatLng();
|
||||
applyHeatmapGlow(contextId, latlng.lat, latlng.lng);
|
||||
}
|
||||
} catch (e) { /* ignore malformed payloads */ }
|
||||
});
|
||||
heatmapDecayTimer = setInterval(function () {
|
||||
var changed = false;
|
||||
for (var id in heatmapCounts) {
|
||||
heatmapCounts[id] = Math.floor(heatmapCounts[id] * 0.8);
|
||||
if (heatmapCounts[id] === 0) {
|
||||
delete heatmapCounts[id];
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
heatmapMaxCount = 1;
|
||||
for (var id2 in heatmapCounts) {
|
||||
if (heatmapCounts[id2] > heatmapMaxCount) {
|
||||
heatmapMaxCount = heatmapCounts[id2];
|
||||
}
|
||||
}
|
||||
refreshAllGlows();
|
||||
}
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
function disconnectHeatmap() {
|
||||
if (heatmapSocket) {
|
||||
heatmapSocket.disconnect();
|
||||
heatmapSocket = null;
|
||||
}
|
||||
if (heatmapDecayTimer) {
|
||||
clearInterval(heatmapDecayTimer);
|
||||
heatmapDecayTimer = null;
|
||||
}
|
||||
heatmapCounts = {};
|
||||
heatmapMaxCount = 1;
|
||||
clearHeatmapVisuals();
|
||||
}
|
||||
|
||||
// === Events ===
|
||||
map.on("moveend", function () {
|
||||
clearTimeout(debounceTimer);
|
||||
|
|
@ -593,6 +724,11 @@
|
|||
showHotspots.addEventListener("change", refetchActive);
|
||||
showInactive.addEventListener("change", refetchActive);
|
||||
|
||||
showHeatmap.addEventListener("change", function () {
|
||||
if (showHeatmap.checked) connectHeatmap();
|
||||
else disconnectHeatmap();
|
||||
});
|
||||
|
||||
pinClearBtn.addEventListener("click", clearPin);
|
||||
|
||||
pinRadiusInput.addEventListener("change", function () {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@
|
|||
<label class="hotspot-label"><input type="checkbox" id="show-hotspots"> Don't hide personal hotspots</label>
|
||||
<label class="hotspot-label"><input type="checkbox" id="show-inactive"> Show inactive repeaters</label>
|
||||
</details>
|
||||
<details class="experimental">
|
||||
<summary>Experimental</summary>
|
||||
<label class="hotspot-label"><input type="checkbox" id="show-heatmap"> Visualize Last Heard Heatmap</label>
|
||||
</details>
|
||||
<div class="controls-row route-row autocomplete-wrap">
|
||||
<input type="text" id="route-from" placeholder="From address" autocomplete="off" />
|
||||
<ul class="autocomplete-list" id="ac-from"></ul>
|
||||
|
|
@ -58,6 +62,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<script src="lib/leaflet.js"></script>
|
||||
<script src="lib/socket.io.min.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
7
static/lib/socket.io.min.js
vendored
Normal file
7
static/lib/socket.io.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -201,6 +201,27 @@ html, body {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.experimental {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--text-fainter);
|
||||
}
|
||||
|
||||
.experimental summary {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.experimental summary:hover {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.experimental[open] {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.count {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
|
|
@ -488,6 +509,16 @@ html, body {
|
|||
background: #D32F2F;
|
||||
}
|
||||
|
||||
@keyframes heatmap-pulse {
|
||||
0% { opacity: 0.8; }
|
||||
50% { opacity: 0.3; }
|
||||
100% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
.heatmap-glow {
|
||||
animation: heatmap-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#controls {
|
||||
top: 10px;
|
||||
|
|
|
|||
Loading…
Reference in a new issue