mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
201 lines
6.3 KiB
HTML
201 lines
6.3 KiB
HTML
{% extends "base.html" %}
|
|
{% block css %}
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
crossorigin=""/>
|
|
<style>
|
|
.legend {
|
|
background: white;
|
|
padding: 8px;
|
|
line-height: 1.5;
|
|
border-radius: 5px;
|
|
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
|
font-size: 14px;
|
|
color: black;
|
|
}
|
|
.legend i {
|
|
width: 12px;
|
|
height: 12px;
|
|
display: inline-block;
|
|
margin-right: 6px;
|
|
border-radius: 50%;
|
|
}
|
|
#filter-container {
|
|
text-align: center;
|
|
margin-top: 10px;
|
|
}
|
|
.filter-checkbox {
|
|
margin: 0 10px;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block body %}
|
|
<div id="map" style="width: 100%; height: 600px;"></div>
|
|
|
|
<div id="filter-container">
|
|
<!-- Filters will be dynamically generated here -->
|
|
</div>
|
|
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""></script>
|
|
|
|
<div id="filter-container">
|
|
<input type="checkbox" class="filter-checkbox" id="filter-routers-only"> Show Routers Only
|
|
</div>
|
|
|
|
<script>
|
|
var map = L.map('map');
|
|
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
}).addTo(map);
|
|
|
|
var markers = {};
|
|
var bounds = L.latLngBounds();
|
|
var channels = new Set(); // Stores unique channels
|
|
|
|
var nodes = [
|
|
{% for node in nodes %}
|
|
{
|
|
lat: {{ node.last_lat / 10**7 if node.last_lat else "null" }},
|
|
long: {{ node.last_long / 10**7 if node.last_long else "null" }},
|
|
long_name: "{{ node.long_name | escape }}",
|
|
short_name: "{{ node.short_name | escape }}",
|
|
channel: "{{ node.channel | escape }}",
|
|
hw_model: "{{ node.hw_model | escape }}",
|
|
role: "{{ node.role | escape }}",
|
|
last_update: "{{ node.last_update | escape }}",
|
|
firmware: "{{ node.firmware | escape }}",
|
|
id: "{{ node.node_id }}",
|
|
isRouter: "{{ 'router' in node.role.lower() }}"
|
|
},
|
|
{% endfor %}
|
|
];
|
|
|
|
function timeAgo(date) {
|
|
var now = new Date();
|
|
var diff = now - new Date(date);
|
|
var seconds = Math.floor(diff / 1000);
|
|
var minutes = Math.floor(seconds / 60);
|
|
var hours = Math.floor(minutes / 60);
|
|
var days = Math.floor(hours / 24);
|
|
|
|
if (days > 0) return days + "d";
|
|
if (hours > 0) return hours + "h";
|
|
if (minutes > 0) return minutes + "m";
|
|
return seconds + "s";
|
|
}
|
|
|
|
const palette = [
|
|
"#e6194b", // red
|
|
"#4363d8", // blue
|
|
"#f58231", // orange
|
|
"#911eb4", // purple
|
|
"#46f0f0", // cyan
|
|
"#f032e6", // magenta
|
|
"#bcf60c", // lime
|
|
"#fabebe", // pink
|
|
"#008080", // teal
|
|
"#e6beff", // lavender
|
|
"#9a6324", // brown
|
|
"#fffac8", // cream
|
|
"#800000", // maroon
|
|
"#aaffc3", // mint
|
|
"#808000", // olive
|
|
"#ffd8b1", // apricot
|
|
"#000075", // navy
|
|
"#808080" // gray
|
|
];
|
|
|
|
const colorMap = new Map();
|
|
let nextColorIndex = 0;
|
|
|
|
function hashToColor(str) {
|
|
if (colorMap.has(str)) return colorMap.get(str);
|
|
|
|
const color = palette[nextColorIndex % palette.length];
|
|
colorMap.set(str, color);
|
|
nextColorIndex++;
|
|
return color;
|
|
}
|
|
|
|
nodes.forEach(function(node) {
|
|
if (node.lat !== null && node.long !== null) {
|
|
let category = node.channel;
|
|
let isRouter = node.isRouter === "True"; // Convert from string
|
|
|
|
channels.add(category);
|
|
|
|
let color = hashToColor(category);
|
|
|
|
let markerOptions = {
|
|
radius: isRouter ? 9 : 7,
|
|
color: "white",
|
|
fillColor: color,
|
|
fillOpacity: 1,
|
|
weight: 0.7,
|
|
};
|
|
|
|
var popupContent = `
|
|
<b><a href="/packet_list/${node.id}">${node.long_name}</a> (${node.short_name})</b><br>
|
|
<b>Channel:</b> ${node.channel}<br>
|
|
<b>Model:</b> ${node.hw_model}<br>
|
|
<b>Role:</b> ${node.role}<br>
|
|
`;
|
|
if (node.last_update) popupContent += `<b>Last seen:</b> ${timeAgo(node.last_update)}<br>`;
|
|
if (node.firmware) popupContent += `<b>Firmware:</b> ${node.firmware}<br>`;
|
|
|
|
var marker = L.circleMarker([node.lat, node.long], markerOptions).bindPopup(popupContent);
|
|
marker.addTo(map);
|
|
|
|
if (!markers[category]) markers[category] = [];
|
|
markers[category].push({ marker, isRouter });
|
|
|
|
bounds.extend(marker.getLatLng());
|
|
}
|
|
});
|
|
|
|
var bayAreaBounds = [
|
|
[{{ site_config["site"]["map_top_left_lat"] }}, {{ site_config["site"]["map_top_left_lon"] }}],
|
|
[{{ site_config["site"]["map_bottom_right_lat"] }}, {{ site_config["site"]["map_bottom_right_lon"] }}]
|
|
];
|
|
map.fitBounds(bayAreaBounds);
|
|
|
|
let filterContainer = document.getElementById("filter-container");
|
|
|
|
channels.forEach(channel => {
|
|
let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`;
|
|
let color = hashToColor(channel);
|
|
let filterHtml = `
|
|
<label style="color:${color};">
|
|
<input type="checkbox" class="filter-checkbox" id="${filterId}" checked> ${channel}
|
|
</label>
|
|
`;
|
|
filterContainer.innerHTML += filterHtml;
|
|
});
|
|
|
|
function updateMarkers() {
|
|
let showRoutersOnly = document.getElementById("filter-routers-only").checked;
|
|
|
|
channels.forEach(channel => {
|
|
let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`;
|
|
let isChecked = document.getElementById(filterId).checked;
|
|
|
|
markers[channel].forEach(obj => {
|
|
let shouldShow = isChecked && (!showRoutersOnly || obj.isRouter);
|
|
shouldShow ? map.addLayer(obj.marker) : map.removeLayer(obj.marker);
|
|
});
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll(".filter-checkbox").forEach(input => {
|
|
input.addEventListener("change", updateMarkers);
|
|
});
|
|
|
|
</script>
|
|
|
|
{% endblock %}
|