mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
Update multi-language support. So far Spanish and english.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin=""/>
|
||||
<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%; }
|
||||
@@ -9,7 +11,8 @@
|
||||
#filter-container { text-align:center;margin-top:10px; }
|
||||
.filter-checkbox { margin:0 10px; }
|
||||
|
||||
#share-button, #reset-filters-button {
|
||||
#share-button,
|
||||
#reset-filters-button {
|
||||
padding:5px 15px;border:none;border-radius:4px;font-size:14px;cursor:pointer;color:white;
|
||||
}
|
||||
#share-button { margin-left:20px; background-color:#4CAF50; }
|
||||
@@ -26,10 +29,15 @@
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div id="map" style="width:100%;height:calc(100vh - 270px)"></div>
|
||||
|
||||
<!-- ⭐ Map Legend ⭐ -->
|
||||
<div id="map-legend" class="legend" style="position:absolute; bottom:30px; right:15px; z-index:1000;">
|
||||
<div id="map" style="width:100%; height:calc(100vh - 270px)"></div>
|
||||
<!-- ⭐ Map Legend (safe z-index below blinks) ⭐ -->
|
||||
<div id="map-legend"
|
||||
class="legend"
|
||||
style="position:absolute;
|
||||
bottom:30px;
|
||||
right:15px;
|
||||
z-index:500; /* ✔ Below Leaflet tooltips */
|
||||
pointer-events:none;">
|
||||
<div>
|
||||
<i style="background:orange; width:15px; height:3px; border-radius:0;"></i>
|
||||
<span data-translate-lang="legend_traceroute">Traceroute Path (arrowed)</span>
|
||||
@@ -41,31 +49,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ⭐ Router Filter ⭐ -->
|
||||
<div id="filter-container">
|
||||
<input type="checkbox" class="filter-checkbox" id="filter-routers-only">
|
||||
<span data-translate-lang="filter_routers_only">Show Routers Only</span>
|
||||
<span data-translate-lang="show_routers_only">Show Routers Only</span>
|
||||
</div>
|
||||
|
||||
<!-- ⭐ Share + Reset ⭐ -->
|
||||
<div style="text-align:center;margin-top:5px;">
|
||||
<button id="share-button" onclick="shareCurrentView()" data-translate-lang="share_view">
|
||||
🔗 Share This View
|
||||
</button>
|
||||
|
||||
<button id="reset-filters-button" onclick="resetFiltersToDefaults()" data-translate-lang="reset_filters">
|
||||
↺ Reset Filters To Defaults
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- JS Includes -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||||
<script src="https://unpkg.com/leaflet-polylinedecorator@1.6.0/dist/leaflet.polylinedecorator.js" crossorigin=""></script>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
|
||||
<script src="https://unpkg.com/leaflet-polylinedecorator@1.6.0/dist/leaflet.polylinedecorator.js"
|
||||
integrity="sha384-FhPn/2P/fJGhQLeNWDn9B/2Gml2bPOrKJwFqJXgR3xOPYxWg5mYQ5XZdhUSugZT0"
|
||||
crossorigin></script>
|
||||
|
||||
<script>
|
||||
/* ===========================================================
|
||||
TRANSLATION LOADING
|
||||
=========================================================== */
|
||||
/* ======================================================
|
||||
MAP PAGE TRANSLATION SYSTEM
|
||||
====================================================== */
|
||||
|
||||
let mapTranslations = {};
|
||||
|
||||
@@ -73,7 +82,7 @@ async function loadTranslationsMap() {
|
||||
try {
|
||||
const cfg = await window._siteConfigPromise;
|
||||
const lang = cfg?.site?.language || "en";
|
||||
const res = await fetch(`/api/lang?lang=${lang}§ion=map`);
|
||||
const res = await fetch(`/api/lang?lang=${lang}§ion=map`);
|
||||
mapTranslations = await res.json();
|
||||
applyTranslationsMap();
|
||||
} catch (err) {
|
||||
@@ -81,258 +90,339 @@ async function loadTranslationsMap() {
|
||||
}
|
||||
}
|
||||
|
||||
function applyTranslationsMap() {
|
||||
document.querySelectorAll("[data-translate-lang]").forEach(el => {
|
||||
function applyTranslationsMap(root = document) {
|
||||
root.querySelectorAll("[data-translate-lang]").forEach(el => {
|
||||
const key = el.dataset.translateLang;
|
||||
if (!mapTranslations[key]) return;
|
||||
const val = mapTranslations[key];
|
||||
if (!val) return;
|
||||
|
||||
if (el.tagName === "INPUT" && el.placeholder)
|
||||
el.placeholder = mapTranslations[key];
|
||||
else
|
||||
el.textContent = mapTranslations[key];
|
||||
if (el.tagName === "INPUT" && el.placeholder !== undefined) {
|
||||
el.placeholder = val;
|
||||
} else {
|
||||
el.textContent = val;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
MAP SETUP
|
||||
=========================================================== */
|
||||
/* ======================================================
|
||||
EXISTING MAP LOGIC (UNCHANGED)
|
||||
====================================================== */
|
||||
|
||||
// ---------------------- Map Initialization ----------------------
|
||||
var map = L.map('map');
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom:19 }).addTo(map);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom:19, attribution:'© OpenStreetMap' }).addTo(map);
|
||||
|
||||
var nodes=[], markerById={}, nodeMap=new Map();
|
||||
var edgesData=[], edgeLayer=L.layerGroup().addTo(map);
|
||||
var selectedNodeId = null;
|
||||
|
||||
var activeBlinks=new Map();
|
||||
var lastImportTime=null;
|
||||
var mapInterval=0;
|
||||
// ---------------------- Globals ----------------------
|
||||
var nodes=[], markers={}, markerById={}, nodeMap = new Map();
|
||||
var edgesData=[], edgeLayer = L.layerGroup().addTo(map), selectedNodeId = null;
|
||||
var activeBlinks = new Map(), lastImportTime = null;
|
||||
var mapInterval = 0;
|
||||
|
||||
const portMap = {
|
||||
1:"Text", 67:"Telemetry", 3:"Position", 70:"Traceroute",
|
||||
4:"Node Info", 71:"Neighbour Info", 73:"Map Report"
|
||||
1:"Text",
|
||||
67:"Telemetry",
|
||||
3:"Position",
|
||||
70:"Traceroute",
|
||||
4:"Node Info",
|
||||
71:"Neighbour Info",
|
||||
73:"Map Report"
|
||||
};
|
||||
|
||||
const palette=["#e6194b","#4363d8","#f58231","#911eb4","#46f0f0","#f032e6",
|
||||
"#bcf60c","#fabebe","#008080","#e6beff","#9a6324","#fffac8",
|
||||
"#800000","#aaffc3","#808000","#ffd8b1","#000075","#808080"];
|
||||
const palette = ["#e6194b","#4363d8","#f58231","#911eb4","#46f0f0","#f032e6","#bcf60c","#fabebe",
|
||||
"#008080","#e6beff","#9a6324","#fffac8","#800000","#aaffc3","#808000","#ffd8b1",
|
||||
"#000075","#808080"];
|
||||
|
||||
const colorMap=new Map(); let nextColorIndex=0;
|
||||
const channelSet=new Set();
|
||||
const colorMap = new Map(); let nextColorIndex = 0;
|
||||
const channelSet = new Set();
|
||||
|
||||
function hashColor(s){
|
||||
if(colorMap.has(s)) return colorMap.get(s);
|
||||
let c = palette[nextColorIndex++ % palette.length];
|
||||
colorMap.set(s,c);
|
||||
function timeAgo(date){
|
||||
const diff = Date.now() - new Date(date);
|
||||
const s = Math.floor(diff/1000), m = Math.floor(s/60),
|
||||
h = Math.floor(m/60), d = Math.floor(h/24);
|
||||
return d>0?d+"d":h>0?h+"h":m>0?m+"m":s+"s";
|
||||
}
|
||||
|
||||
function hashToColor(str){
|
||||
if(colorMap.has(str)) return colorMap.get(str);
|
||||
const c = palette[nextColorIndex++ % palette.length];
|
||||
colorMap.set(str,c);
|
||||
return c;
|
||||
}
|
||||
|
||||
function isBad(n) {
|
||||
return !n || !n.lat || !n.long || n.lat===0 || n.long===0;
|
||||
function isInvalidCoord(n){
|
||||
return !n||!n.lat||!n.long||n.lat===0||n.long===0||Number.isNaN(n.lat)||Number.isNaN(n.long);
|
||||
}
|
||||
|
||||
function timeAgo(ts) {
|
||||
const diff = Date.now() - new Date(ts);
|
||||
const s=Math.floor(diff/1000);
|
||||
const m=Math.floor(s/60);
|
||||
const h=Math.floor(m/60);
|
||||
const d=Math.floor(h/24);
|
||||
return d>0?(d+"d"):h>0?(h+"h"):m>0?(m+"m"):(s+"s");
|
||||
// ---------------------- Packet Fetching ----------------------
|
||||
function fetchLatestPacket(){
|
||||
fetch(`/api/packets?limit=1`)
|
||||
.then(r=>r.json())
|
||||
.then(data=>{
|
||||
lastImportTime=data.packets?.[0]?.import_time_us||0;
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
RESPECT CONFIG ZOOM
|
||||
=========================================================== */
|
||||
function fetchNewPackets(){
|
||||
if(mapInterval <= 0) return;
|
||||
if(lastImportTime===null) return;
|
||||
|
||||
const url = new URL(`/api/packets`, window.location.origin);
|
||||
url.searchParams.set("since", lastImportTime);
|
||||
url.searchParams.set("limit", 50);
|
||||
|
||||
fetch(url)
|
||||
.then(r=>r.json())
|
||||
.then(data=>{
|
||||
if(!data.packets || data.packets.length===0) return;
|
||||
let latest = lastImportTime;
|
||||
data.packets.forEach(pkt=>{
|
||||
if(pkt.import_time_us > latest) latest = pkt.import_time_us;
|
||||
const marker = markerById[pkt.from_node_id];
|
||||
const nodeData = nodeMap.get(pkt.from_node_id);
|
||||
if(marker && nodeData) blinkNode(marker,nodeData.long_name,pkt.portnum);
|
||||
});
|
||||
lastImportTime = latest;
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
// ---------------------- Polling ----------------------
|
||||
let packetInterval=null;
|
||||
|
||||
function startPacketFetcher(){
|
||||
if(mapInterval<=0) return;
|
||||
if(!packetInterval){
|
||||
fetchLatestPacket();
|
||||
packetInterval=setInterval(fetchNewPackets,mapInterval*1000);
|
||||
}
|
||||
}
|
||||
|
||||
function stopPacketFetcher(){
|
||||
if(packetInterval){
|
||||
clearInterval(packetInterval);
|
||||
packetInterval=null;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("visibilitychange",()=>{
|
||||
document.hidden?stopPacketFetcher():startPacketFetcher();
|
||||
});
|
||||
|
||||
// ---------------------- WAIT FOR CONFIG ----------------------
|
||||
async function waitForConfig() {
|
||||
while (!window._siteConfigPromise)
|
||||
await new Promise(r=>setTimeout(r,100));
|
||||
const cfg = await window._siteConfigPromise;
|
||||
return cfg.site || {};
|
||||
}
|
||||
|
||||
async function initMapView() {
|
||||
const site = await waitForConfig();
|
||||
mapInterval = parseInt(site.map_interval,10) || 0;
|
||||
|
||||
// URL parameters override config
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const lat = parseFloat(params.get("lat"));
|
||||
const lng = parseFloat(params.get("lng"));
|
||||
const zoom = parseInt(params.get("zoom"),10);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng) && !isNaN(zoom)) {
|
||||
map.setView([lat,lng],zoom);
|
||||
window.configBoundsApplied = true;
|
||||
} else {
|
||||
const tl = [parseFloat(site.map_top_left_lat), parseFloat(site.map_top_left_lon)];
|
||||
const br = [parseFloat(site.map_bottom_right_lat), parseFloat(site.map_bottom_right_lon)];
|
||||
if (tl.every(isFinite) && br.every(isFinite)) {
|
||||
map.fitBounds([tl,br]);
|
||||
window.configBoundsApplied = true;
|
||||
}
|
||||
while (typeof window._siteConfigPromise === "undefined") {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
|
||||
setTimeout(()=>map.invalidateSize(),200);
|
||||
try {
|
||||
const cfg = await window._siteConfigPromise;
|
||||
return cfg.site || {};
|
||||
} catch (err) {
|
||||
console.error("Error loading site config:", err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
initMapView();
|
||||
// ---------------------- Load Config & Start Polling ----------------------
|
||||
async function initMapPolling() {
|
||||
try {
|
||||
const site = await waitForConfig();
|
||||
mapInterval = parseInt(site.map_interval, 10) || 0;
|
||||
|
||||
/* ===========================================================
|
||||
LOAD NODES + EDGES
|
||||
=========================================================== */
|
||||
// --- URL params ---
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const lat = parseFloat(params.get('lat'));
|
||||
const lng = parseFloat(params.get('lng'));
|
||||
const zoom = parseInt(params.get('zoom'), 10);
|
||||
|
||||
fetch("/api/nodes?days_active=3")
|
||||
.then(r=>r.json())
|
||||
.then(data=>{
|
||||
nodes = (data.nodes||[]).map(n => ({
|
||||
key: n.node_id ?? n.id,
|
||||
node_id: n.node_id,
|
||||
lat: n.last_lat ? n.last_lat/1e7 : null,
|
||||
long: n.last_long ? n.last_long/1e7 : null,
|
||||
long_name: n.long_name || "",
|
||||
short_name: n.short_name || "",
|
||||
channel: n.channel || "",
|
||||
hw_model: n.hw_model || "",
|
||||
role: n.role || "",
|
||||
firmware: n.firmware || "",
|
||||
last_update: n.last_update || "",
|
||||
isRouter: (n.role||"").toLowerCase().includes("router")
|
||||
}));
|
||||
if (!isNaN(lat) && !isNaN(lng) && !isNaN(zoom)) {
|
||||
map.setView([lat, lng], zoom);
|
||||
window.configBoundsApplied = true;
|
||||
setTimeout(() => map.invalidateSize(), 100);
|
||||
}
|
||||
else {
|
||||
const tl = [parseFloat(site.map_top_left_lat), parseFloat(site.map_top_left_lon)];
|
||||
const br = [parseFloat(site.map_bottom_right_lat), parseFloat(site.map_bottom_right_lon)];
|
||||
if (tl.every(isFinite) && br.every(isFinite)) {
|
||||
map.fitBounds([tl, br]);
|
||||
window.configBoundsApplied = true;
|
||||
setTimeout(() => map.invalidateSize(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
nodes.forEach(n => {
|
||||
nodeMap.set(n.key,n);
|
||||
if (n.channel) channelSet.add(n.channel);
|
||||
});
|
||||
if (mapInterval > 0) startPacketFetcher();
|
||||
|
||||
renderNodes();
|
||||
createChannelFilters();
|
||||
} catch (err) {
|
||||
console.error("Failed to load /api/config:", err);
|
||||
}
|
||||
}
|
||||
|
||||
return fetch("/api/edges");
|
||||
})
|
||||
.then(r=>r.json())
|
||||
.then(data => edgesData = data.edges || []);
|
||||
initMapPolling();
|
||||
|
||||
function renderNodes(){
|
||||
const bounds = L.latLngBounds();
|
||||
// ---------------------- Load Nodes + Edges ----------------------
|
||||
fetch('/api/nodes?days_active=3')
|
||||
.then(r=>r.json())
|
||||
.then(data=>{
|
||||
if(!data.nodes) return;
|
||||
|
||||
nodes.forEach(n=>{
|
||||
if (isBad(n)) return;
|
||||
nodes = data.nodes.map(n=>({
|
||||
key: n.node_id??n.id,
|
||||
id: n.id,
|
||||
node_id: n.node_id,
|
||||
lat: n.last_lat? n.last_lat/1e7 : null,
|
||||
long: n.last_long? n.last_long/1e7 : null,
|
||||
long_name: n.long_name||"",
|
||||
short_name: n.short_name||"",
|
||||
channel: n.channel||"",
|
||||
hw_model: n.hw_model||"",
|
||||
role: n.role||"",
|
||||
firmware: n.firmware||"",
|
||||
last_update: n.last_update||"",
|
||||
isRouter: (n.role||"").toLowerCase().includes("router")
|
||||
}));
|
||||
|
||||
const color = hashColor(n.channel);
|
||||
const radius = n.isRouter ? 9 : 7;
|
||||
|
||||
const marker = L.circleMarker([n.lat,n.long],{
|
||||
radius, color:"white",
|
||||
fillColor:color, fillOpacity:1,
|
||||
weight:0.7
|
||||
}).addTo(map);
|
||||
|
||||
markerById[n.key] = marker;
|
||||
marker.originalColor = color;
|
||||
|
||||
const popup = `
|
||||
<b><a href="/node/${n.node_id}">${n.long_name}</a> (${n.short_name})</b><br>
|
||||
<b data-translate-lang="channel_label"></b> ${n.channel}<br>
|
||||
<b data-translate-lang="model_label"></b> ${n.hw_model}<br>
|
||||
<b data-translate-lang="role_label"></b> ${n.role}<br>
|
||||
|
||||
${n.last_update
|
||||
? `<b data-translate-lang="last_seen"></b> ${timeAgo(n.last_update)}<br>` : ""}
|
||||
|
||||
${n.firmware
|
||||
? `<b data-translate-lang="firmware"></b> ${n.firmware}<br>` : ""}
|
||||
`;
|
||||
|
||||
marker.on("click", ()=>{
|
||||
onNodeClick(n);
|
||||
marker.bindPopup(popup).openPopup();
|
||||
setTimeout(applyTranslationsMap,10);
|
||||
nodes.forEach(n=>{
|
||||
nodeMap.set(n.key,n);
|
||||
if(n.channel) channelSet.add(n.channel);
|
||||
});
|
||||
|
||||
bounds.extend(marker.getLatLng());
|
||||
renderNodesOnMap();
|
||||
createChannelFilters();
|
||||
|
||||
return fetch('/api/edges');
|
||||
})
|
||||
.then(r=>r?r.json():null)
|
||||
.then(data=>{
|
||||
if(data && data.edges) edgesData=data.edges;
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
// ---------------------- Render Nodes ----------------------
|
||||
function renderNodesOnMap(){
|
||||
nodes.forEach(node=>{
|
||||
if(isInvalidCoord(node)) return;
|
||||
|
||||
const color = hashToColor(node.channel);
|
||||
|
||||
const marker = L.circleMarker([node.lat,node.long], {
|
||||
radius: node.isRouter ? 9 : 7,
|
||||
color: "white",
|
||||
fillColor: color,
|
||||
fillOpacity: 1,
|
||||
weight: 0.7
|
||||
}).addTo(map);
|
||||
|
||||
marker.nodeId = node.key;
|
||||
marker.originalColor = color;
|
||||
markerById[node.key] = marker;
|
||||
|
||||
const popup = `
|
||||
<b><a href="/node/${node.node_id}">${node.long_name}</a> (${node.short_name})</b><br>
|
||||
|
||||
<b data-translate-lang="channel_label"></b> ${node.channel}<br>
|
||||
<b data-translate-lang="model_label"></b> ${node.hw_model}<br>
|
||||
<b data-translate-lang="role_label"></b> ${node.role}<br>
|
||||
|
||||
${
|
||||
node.last_update
|
||||
? `<b data-translate-lang="last_seen"></b> ${timeAgo(node.last_update)}<br>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
node.firmware
|
||||
? `<b data-translate-lang="firmware"></b> ${node.firmware}<br>`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
|
||||
marker.on("click", () => {
|
||||
onNodeClick(node);
|
||||
marker.bindPopup(popup).openPopup();
|
||||
setTimeout(() => marker.closePopup(), 3000);
|
||||
});
|
||||
});
|
||||
|
||||
/* ⭐ DO NOT RE-ZOOM IF CONFIG/URL VIEW WAS APPLIED ⭐ */
|
||||
if (!window.configBoundsApplied && bounds.isValid()) {
|
||||
map.fitBounds(bounds);
|
||||
setTimeout(()=>map.invalidateSize(),100);
|
||||
}
|
||||
// ❌ DO NOT auto-fit:
|
||||
// map.fitBounds(bounds)
|
||||
|
||||
// Still apply translations for popup content
|
||||
setTimeout(() => applyTranslationsMap(), 50);
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
EDGES
|
||||
=========================================================== */
|
||||
|
||||
function onNodeClick(n) {
|
||||
selectedNodeId = n.key;
|
||||
// ---------------------- Render Edges ----------------------
|
||||
function onNodeClick(node){
|
||||
selectedNodeId = node.key;
|
||||
edgeLayer.clearLayers();
|
||||
|
||||
edgesData.forEach(edge => {
|
||||
if (edge.from !== n.key && edge.to !== n.key) return;
|
||||
edgesData.forEach(edge=>{
|
||||
if(edge.from !== node.key && edge.to !== node.key) return;
|
||||
|
||||
const f = nodeMap.get(edge.from);
|
||||
const t = nodeMap.get(edge.to);
|
||||
if (!f || !t || isBad(f) || isBad(t)) return;
|
||||
const f=nodeMap.get(edge.from);
|
||||
const t=nodeMap.get(edge.to);
|
||||
|
||||
const color = edge.type === "neighbor" ? "gray" : "orange";
|
||||
if(!f || !t || isInvalidCoord(f) || isInvalidCoord(t)) return;
|
||||
|
||||
const line = L.polyline([[f.lat,f.long],[t.lat,t.long]],{
|
||||
color, weight:3
|
||||
}).addTo(edgeLayer);
|
||||
const color = edge.type==="neighbor" ? "gray" : "orange";
|
||||
const line = L.polyline([[f.lat,f.long],[t.lat,t.long]],{color,weight:3}).addTo(edgeLayer);
|
||||
|
||||
if (edge.type === "traceroute") {
|
||||
if(edge.type==="traceroute"){
|
||||
L.polylineDecorator(line,{
|
||||
patterns:[{
|
||||
offset:'100%',
|
||||
symbol:L.Symbol.arrowHead({
|
||||
pixelSize:6,
|
||||
pathOptions:{color}
|
||||
})
|
||||
}]
|
||||
patterns:[
|
||||
{
|
||||
offset:'100%',
|
||||
repeat:0,
|
||||
symbol:L.Symbol.arrowHead({
|
||||
pixelSize:5,polygon:false,
|
||||
pathOptions:{stroke:true,color}
|
||||
})
|
||||
}
|
||||
]
|
||||
}).addTo(edgeLayer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
map.on("click", e=>{
|
||||
if (!e.originalEvent.target.classList.contains("leaflet-interactive")) {
|
||||
map.on('click',e=>{
|
||||
if(!e.originalEvent.target.classList.contains('leaflet-interactive')){
|
||||
edgeLayer.clearLayers();
|
||||
selectedNodeId=null;
|
||||
}
|
||||
});
|
||||
|
||||
/* ===========================================================
|
||||
BLINKING NODES
|
||||
=========================================================== */
|
||||
|
||||
function blinkNode(marker, longName, portnum){
|
||||
if (!map.hasLayer(marker)) return;
|
||||
|
||||
if (activeBlinks.has(marker)) {
|
||||
// ---------------------- Blinking ----------------------
|
||||
function blinkNode(marker,longName,portnum){
|
||||
if(!map.hasLayer(marker)) return;
|
||||
if(activeBlinks.has(marker)){
|
||||
clearInterval(activeBlinks.get(marker));
|
||||
marker.setStyle({fillColor:marker.originalColor});
|
||||
if(marker.tooltip) map.removeLayer(marker.tooltip);
|
||||
}
|
||||
|
||||
const portLabel = portMap[portnum] || ("Port "+portnum);
|
||||
let blinkCount=0;
|
||||
const portName = portMap[portnum] || `Port ${portnum}`;
|
||||
|
||||
const tooltip = L.tooltip({
|
||||
permanent:true,
|
||||
direction:'top',
|
||||
offset:[0,-marker.options.radius-6],
|
||||
offset:[0,-marker.options.radius-5],
|
||||
className:'blinking-tooltip'
|
||||
})
|
||||
.setContent(`${longName} (${portLabel})`)
|
||||
.setLatLng(marker.getLatLng())
|
||||
.addTo(map);
|
||||
}).setContent(`${longName} (${portName})`)
|
||||
.setLatLng(marker.getLatLng())
|
||||
.addTo(map);
|
||||
|
||||
let count=0;
|
||||
const interval=setInterval(()=>{
|
||||
marker.setStyle({
|
||||
fillColor: (count % 2 === 0 ? "yellow" : marker.originalColor)
|
||||
});
|
||||
count++;
|
||||
if (count > 7) {
|
||||
marker.tooltip = tooltip;
|
||||
|
||||
const interval = setInterval(()=>{
|
||||
if(map.hasLayer(marker)){
|
||||
marker.setStyle({
|
||||
fillColor: blinkCount%2===0 ? 'yellow' : marker.originalColor
|
||||
});
|
||||
marker.bringToFront();
|
||||
}
|
||||
blinkCount++;
|
||||
if(blinkCount>7){
|
||||
clearInterval(interval);
|
||||
marker.setStyle({fillColor:marker.originalColor});
|
||||
map.removeLayer(tooltip);
|
||||
@@ -343,99 +433,100 @@ function blinkNode(marker, longName, portnum){
|
||||
activeBlinks.set(marker,interval);
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
CHANNEL FILTERS
|
||||
=========================================================== */
|
||||
|
||||
function createChannelFilters() {
|
||||
const cont = document.getElementById("filter-container");
|
||||
// ---------------------- Channel Filters ----------------------
|
||||
function createChannelFilters(){
|
||||
const filterContainer = document.getElementById("filter-container");
|
||||
const saved = JSON.parse(localStorage.getItem("mapFilters") || "{}");
|
||||
|
||||
channelSet.forEach(ch=>{
|
||||
const cb=document.createElement("input");
|
||||
cb.type="checkbox";
|
||||
cb.className="filter-checkbox";
|
||||
cb.id="filter-"+ch;
|
||||
cb.checked = saved[ch] !== false;
|
||||
channelSet.forEach(channel=>{
|
||||
const cb = document.createElement("input");
|
||||
cb.type = "checkbox";
|
||||
cb.className = "filter-checkbox";
|
||||
cb.id = `filter-channel-${channel}`;
|
||||
cb.checked = saved[channel] !== false;
|
||||
|
||||
cb.addEventListener("change",saveFilters);
|
||||
cb.addEventListener("change",updateNodeVisibility);
|
||||
cb.addEventListener("change", saveFiltersToLocalStorage);
|
||||
cb.addEventListener("change", updateNodeVisibility);
|
||||
|
||||
const label=document.createElement("label");
|
||||
label.htmlFor=cb.id;
|
||||
label.innerText = ch;
|
||||
label.style.color = hashColor(ch);
|
||||
filterContainer.appendChild(cb);
|
||||
|
||||
cont.appendChild(cb);
|
||||
cont.appendChild(label);
|
||||
const label = document.createElement("label");
|
||||
label.htmlFor = cb.id;
|
||||
label.innerText = channel;
|
||||
label.style.color = hashToColor(channel);
|
||||
filterContainer.appendChild(label);
|
||||
});
|
||||
|
||||
const routerFilter=document.getElementById("filter-routers-only");
|
||||
routerFilter.checked = saved["routersOnly"] || false;
|
||||
routerFilter.addEventListener("change",saveFilters);
|
||||
routerFilter.addEventListener("change",updateNodeVisibility);
|
||||
const routerOnly = document.getElementById("filter-routers-only");
|
||||
routerOnly.checked = saved["routersOnly"] || false;
|
||||
|
||||
routerOnly.addEventListener("change", saveFiltersToLocalStorage);
|
||||
routerOnly.addEventListener("change", updateNodeVisibility);
|
||||
|
||||
updateNodeVisibility();
|
||||
}
|
||||
|
||||
function saveFilters() {
|
||||
const s={};
|
||||
function saveFiltersToLocalStorage(){
|
||||
const state = {};
|
||||
channelSet.forEach(ch=>{
|
||||
s[ch] = document.getElementById("filter-"+ch).checked;
|
||||
state[ch] = document.getElementById(`filter-channel-${ch}`).checked;
|
||||
});
|
||||
s["routersOnly"]=document.getElementById("filter-routers-only").checked;
|
||||
localStorage.setItem("mapFilters",JSON.stringify(s));
|
||||
state["routersOnly"] = document.getElementById("filter-routers-only").checked;
|
||||
|
||||
localStorage.setItem("mapFilters", JSON.stringify(state));
|
||||
}
|
||||
|
||||
function updateNodeVisibility() {
|
||||
const showRouters = document.getElementById("filter-routers-only").checked;
|
||||
const active = [...channelSet].filter(ch =>
|
||||
document.getElementById("filter-"+ch).checked
|
||||
function updateNodeVisibility(){
|
||||
const routerOnly = document.getElementById("filter-routers-only").checked;
|
||||
const activeChannels = [...channelSet].filter(ch =>
|
||||
document.getElementById(`filter-channel-${ch}`).checked
|
||||
);
|
||||
|
||||
nodes.forEach(n=>{
|
||||
const marker = markerById[n.key];
|
||||
if (!marker) return;
|
||||
const visible = (!showRouters || n.isRouter) && active.includes(n.channel);
|
||||
if (visible) map.addLayer(marker);
|
||||
else map.removeLayer(marker);
|
||||
if(marker){
|
||||
const visible = (!routerOnly || n.isRouter) &&
|
||||
activeChannels.includes(n.channel);
|
||||
|
||||
if(visible) map.addLayer(marker);
|
||||
else map.removeLayer(marker);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
Share + Reset
|
||||
=========================================================== */
|
||||
|
||||
// ---------------------- Share / Reset ----------------------
|
||||
function shareCurrentView() {
|
||||
const c = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
|
||||
const url =
|
||||
`${window.location.origin}/map?lat=${c.lat.toFixed(6)}&lng=${c.lng.toFixed(6)}&zoom=${zoom}`;
|
||||
const url = `${window.location.origin}/map?lat=${c.lat.toFixed(6)}&lng=${c.lng.toFixed(6)}&zoom=${map.getZoom()}`;
|
||||
|
||||
navigator.clipboard.writeText(url).then(()=>{
|
||||
const btn=document.getElementById("share-button");
|
||||
const prev=btn.textContent;
|
||||
btn.textContent = mapTranslations.link_copied || "✓ Link Copied!";
|
||||
setTimeout(()=>btn.textContent=prev,1500);
|
||||
const btn = document.getElementById('share-button');
|
||||
const old = btn.textContent;
|
||||
btn.textContent = '✓ ' + (mapTranslations.link_copied || 'Link Copied!');
|
||||
btn.style.backgroundColor = '#2196F3';
|
||||
setTimeout(()=>{
|
||||
btn.textContent = old;
|
||||
btn.style.backgroundColor = '#4CAF50';
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function resetFiltersToDefaults(){
|
||||
document.getElementById("filter-routers-only").checked=false;
|
||||
channelSet.forEach(ch=>{
|
||||
document.getElementById("filter-"+ch).checked=true;
|
||||
document.getElementById("filter-routers-only").checked = false;
|
||||
channelSet.forEach(ch => {
|
||||
document.getElementById(`filter-channel-${ch}`).checked = true;
|
||||
});
|
||||
saveFilters();
|
||||
saveFiltersToLocalStorage();
|
||||
updateNodeVisibility();
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
INIT
|
||||
=========================================================== */
|
||||
|
||||
document.addEventListener("DOMContentLoaded", loadTranslationsMap);
|
||||
/* ======================================================
|
||||
START TRANSLATION + PAGE LOAD
|
||||
====================================================== */
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
loadTranslationsMap();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user