event-driven node updates and grid-based neighbor finding!

This commit is contained in:
2025-09-09 23:52:19 -05:00
parent 1d4a93a553
commit 6bb19d1844
2 changed files with 148 additions and 100 deletions
+4
View File
@@ -53,6 +53,10 @@
<input type="checkbox" id="showGrid" autocomplete="off"> <input type="checkbox" id="showGrid" autocomplete="off">
<label for="showGrid">Show Grid</label> <label for="showGrid">Show Grid</label>
</div> </div>
<div id="visualizationSettingsToggles">
<input type="checkbox" id="showNodeUpdates" autocomplete="off">
<label for="showNodeUpdates">Show Node Updates</label>
</div>
<div id="visualizationSettingsToggles"> <div id="visualizationSettingsToggles">
<input type="checkbox" id="showTTL" autocomplete="off" checked> <input type="checkbox" id="showTTL" autocomplete="off" checked>
<label for="showTTL">Show TTL Colors</label> <label for="showTTL">Show TTL Colors</label>
+144 -100
View File
@@ -8,6 +8,7 @@ var displayWarningWhenDone = false;
var stillRouting = false; var stillRouting = false;
var markNeighborInterval = null; var markNeighborInterval = null;
var showGrid = false; var showGrid = false;
var showUpdates = false;
// Track all active intervals for marking neighbors, and a cancel flag // Track all active intervals for marking neighbors, and a cancel flag
var markNeighborIntervals = new Set(); var markNeighborIntervals = new Set();
var cancelMarkNeighborRequested = false; var cancelMarkNeighborRequested = false;
@@ -388,121 +389,145 @@ const markNeighborsAsFailed = (nodeId, from, packetId, visited = new Set()) => {
function recomputeNodeConnections(nodeId) { function recomputeNodeConnections(nodeId) {
const p = network.getPosition(nodeId); const p = network.getPosition(nodeId);
grid.update(nodeId, p.x, p.y); const candidates = grid.getNeighborCandidates(p.x, p.y);
}
// main event loop // check all nearby nodes against all other nearby nodes
function main() { for (let a of candidates) {
const currentNodes = network.getPositions(); if (!nodeTable[a]) {
const currentNodesKeys = Object.keys(currentNodes);
for (let node1 of currentNodesKeys) {
let neighbors = [];
if (!nodeTable[node1]) {
continue; continue;
} }
for (let node2 of currentNodesKeys) { // in case user drags node too fast, drop far connections
if (!nodeTable[node2]) { for (let connection of nodeTable[a].connections) {
continue; const aPos = network.getPosition(a);
const bPos = network.getPosition(connection);
const distance = calculateDistance(aPos,bPos);
if (distance > CONNECTION_DISTANCE) {
dropConnection(a,connection);
} }
if (node1 !== node2) { }
const node1data = currentNodes[node1]; if (showUpdates) {
const node2data = currentNodes[node2]; console.log('updating')
const distance = calculateDistance(node1data, node2data); let lastTime;
if (distance < CONNECTION_DISTANCE) { let fadePerS = 10;
neighbors.push({'node': node2, 'distance': distanceToRSSI(distance), }); let currentFade = 0;
let animationId;
function animateFlash(time) {
if (!lastTime) {
lastTime = time;
}
const dTs = (time - lastTime) / 1000;
lastTime = time;
const diff = fadePerS * dTs;
currentFade += diff;
nodes.update({id: a, color: {background: smoothColorTransition('#eb4034', '#97c2fc', 0, 5, currentFade)}})
if (currentFade < 5) {
animationId = requestAnimationFrame(animateFlash);
} else { } else {
edges.remove(`${node1}->${node2}`); nodes.update({id: a, color: {background: '#97c2fc'}});
edges.remove(`${node2}->${node1}`); animationId = null;
const node1Idx = nodeTable[node1].connections.indexOf(node2); }
if (node1Idx !== -1) { }
nodeTable[node1].connections.splice(node1Idx, 1); animationId = requestAnimationFrame(animateFlash);
} }
const node2Idx = nodeTable[node2].connections.indexOf(node1); let trueNeighbors = []
if (node2Idx !== -1) { for (let b of candidates) {
nodeTable[node2].connections.splice(node2Idx, 1); if (a === b) {
} continue;
}
if (!nodeTable[b]) {
continue;
}
const aPos = network.getPosition(a);
const bPos = network.getPosition(b);
const distance = calculateDistance(aPos,bPos);
if (distance < CONNECTION_DISTANCE) {
let connectionScore = 0;
let dropScore = 0;
connectionScore += (100 + distanceToRSSI(distance));
connectionScore += 100 / (nodeTable[b].connections.length + 1);
if (nodeTable[b].connections.length == 0) {
connectionScore += 100;
}
if (nodeTable[a].connections.includes(b) && nodeTable[b].connections.length == 1) {
// assume current node is only connection
connectionScore += 1000;
}
if (nodeTable[a].connections.includes(b)) {
dropScore = connectionScore * DROP_PENALTY;
}
trueNeighbors.push({'nodeId': b, 'distance': distanceToRSSI(distance), 'score': connectionScore, 'dropScore': dropScore});
} else {
if (nodeTable[a].connections.includes(b)) {
dropConnection(a,b);
} }
} }
} }
const scores = []; trueNeighbors.sort((aS, bS) => bS.score - aS.score);
for (let neighbor of neighbors) { const neighborMap = new Map(trueNeighbors.map(n => [n.nodeId, n]));
if (!nodeTable[neighbor.node]) { const topCandidates = trueNeighbors.slice(0, MAX_CONNECTIONS);
let toDrop = new Set();
let toConnect = new Set();
for (let node of topCandidates) {
if (nodeTable[node.nodeId].connections.length >= MAX_CONNECTIONS) {
continue; continue;
} }
let connectionScore = 0; if (nodeTable[a].connections.length >= MAX_CONNECTIONS || (nodeTable[a].connections.length + toConnect.size) >= MAX_CONNECTIONS) {
let dropScore = 0; if (nodeTable[a].connections.includes(node.nodeId)) {
connectionScore += (100 + neighbor.distance); continue;
connectionScore += 100 / (nodeTable[neighbor.node].connections.length + 1); }
if (nodeTable[neighbor.node].connections.length == 0) { for (let activeConnection of nodeTable[a].connections) {
connectionScore += 10000; if (toDrop.has(activeConnection)) {
continue;
}
if (neighborMap.get(activeConnection) && neighborMap.get(activeConnection).dropScore < node.score) {
toDrop.add(activeConnection);
toConnect.add(node.nodeId);
}
}
} else {
toConnect.add(node.nodeId);
} }
if (nodeTable[neighbor.node].connections.length >= MAX_CONNECTIONS) {
continue;
}
if (nodeTable[node1].connections.includes(neighbor.node)) {
dropScore = connectionScore;
}
scores.push({'node': neighbor.node, 'score': connectionScore, 'dropScore': dropScore});
} }
scores.sort((a, b) => b.score - a.score); if (toDrop.size != 0) {
const neighborMap = new Map(neighbors.map(n => [n.node, n])); for (let drop of toDrop) {
const top3 = scores.slice(0, 3); dropConnection(a, drop);
for (let score of top3) { }
if (nodeTable[node1].connections.length >= MAX_CONNECTIONS) { }
let dropped = false; if (toConnect.size != 0) {
if (nodeTable[node1].connections.includes(score.node)) { for (let connect of toConnect) {
//console.log(`${score.node} is already connected; skipping drop attempt`); // if already connected, ignore
continue; if (nodeTable[a].connections.includes(connect)) {
}
for (let existingNode of [...nodeTable[node1].connections]) {
const neighborInfo = neighborMap.get(existingNode);
if (!neighborInfo) {
//console.log(`${existingNode} not a neighbor; skipping`);
continue;
}
let existingScore = 0;
existingScore += (100 + neighborInfo.distance);
existingScore += 100 / (nodeTable[existingNode].connections.length + 1);
if (nodeTable[existingNode].connections.length == 0) {
existingScore += 10000;
}
if (existingScore * DROP_PENALTY < score.score) {
//console.log(`Dropping ${existingNode} from ${node1} for better ${score.node}`);
const node1Idx = nodeTable[node1].connections.indexOf(existingNode);
if (node1Idx !== -1) {
nodeTable[node1].connections.splice(node1Idx, 1);
}
const node2Idx = nodeTable[existingNode].connections.indexOf(node1);
if (node2Idx !== -1) {
nodeTable[existingNode].connections.splice(node2Idx, 1);
}
edges.remove(`${node1}->${existingNode}`);
edges.remove(`${existingNode}->${node1}`);
dropped = true;
break;
} else {
//console.log(`Not dropping ${existingNode} from ${node1}; score ${existingScore.toFixed(2)} >= candidate ${score.score.toFixed(2)}`);
continue;
}
}
// still at capacity and nothing was dropped — skip adding this candidate
if (nodeTable[node1].connections.length >= MAX_CONNECTIONS && !dropped) {
continue; continue;
} }
nodeTable[a].connections.push(connect);
nodeTable[connect].connections.push(a);
connectNodes(a,connect);
} }
if (nodeTable[node1].connections.includes(score.node) || nodeTable[score.node].connections.includes(node1)) {
continue;
}
nodeTable[node1].connections.push(score.node);
nodeTable[score.node].connections.push(node1);
connectNodes(node1, score.node);
} }
} }
setTimeout(main, UPDATE_INTERVAL);
} }
main();
function onNodeAdd() { function dropConnection(nodeA, nodeB) {
edges.remove(`${nodeA}->${nodeB}`);
edges.remove(`${nodeB}->${nodeA}`);
const aIdx = nodeTable[nodeA].connections.indexOf(nodeB);
const bIdx = nodeTable[nodeB].connections.indexOf(nodeA);
if (aIdx !== -1) {
nodeTable[nodeA].connections.splice(aIdx, 1);
}
if (bIdx !== -1) {
nodeTable[nodeB].connections.splice(bIdx, 1);
}
}
function onNodeAdd(nodeId) {
// update grid
const position = network.getPosition(nodeId);
grid.add(nodeId, position.x, position.y);
recomputeNodeConnections(nodeId);
var nodeLength = Object.keys(nodeTable).length; var nodeLength = Object.keys(nodeTable).length;
if (nodeLength >= 200 && !warningModalShown) { if (nodeLength >= 200 && !warningModalShown) {
warningModalShown = true; warningModalShown = true;
@@ -527,9 +552,13 @@ document.getElementById('addNode').addEventListener('click', function() {
const nodeId = generateRealisticLabel(); const nodeId = generateRealisticLabel();
nodes.add({id: nodeId, label: nodeId}); nodes.add({id: nodeId, label: nodeId});
nodeTable[nodeId] = {'connections': [], 'packetCache': [], 'routingTable': {}}; nodeTable[nodeId] = {'connections': [], 'packetCache': [], 'routingTable': {}};
onNodeAdd(); onNodeAdd(nodeId);
}); });
/*
NETWORK EVENTS
*/
network.on('click', function(properties) { network.on('click', function(properties) {
if (properties.nodes.length > 0) { if (properties.nodes.length > 0) {
selectedNode = properties.nodes[0]; selectedNode = properties.nodes[0];
@@ -540,7 +569,7 @@ network.on('click', function(properties) {
const nodeId = generateRealisticLabel(); const nodeId = generateRealisticLabel();
nodes.add({id: nodeId, label: nodeId, x: properties.pointer.canvas.x, y: properties.pointer.canvas.y}); nodes.add({id: nodeId, label: nodeId, x: properties.pointer.canvas.x, y: properties.pointer.canvas.y});
nodeTable[nodeId] = {'connections': [], 'packetCache': [], 'routingTable': {}}; nodeTable[nodeId] = {'connections': [], 'packetCache': [], 'routingTable': {}};
onNodeAdd(); onNodeAdd(nodeId);
} }
}); });
@@ -556,7 +585,17 @@ network.on('doubleClick', function(properties) {
const nodeId = generateRealisticLabel(); const nodeId = generateRealisticLabel();
nodes.add({id: nodeId, label: nodeId, x: properties.pointer.canvas.x, y: properties.pointer.canvas.y}); nodes.add({id: nodeId, label: nodeId, x: properties.pointer.canvas.x, y: properties.pointer.canvas.y});
nodeTable[nodeId] = {'connections': [], 'packetCache': [], 'routingTable': {}}; nodeTable[nodeId] = {'connections': [], 'packetCache': [], 'routingTable': {}};
onNodeAdd(); onNodeAdd(nodeId);
});
network.on('dragging', (properties) => {
if (properties.nodes.length == 0) {
return;
}
const nodeId = properties.nodes[0];
const pos = network.getPosition(nodeId);
grid.update(nodeId, pos.x, pos.y);
recomputeNodeConnections(nodeId);
}); });
network.on('beforeDrawing', function(ctx) { network.on('beforeDrawing', function(ctx) {
@@ -694,7 +733,7 @@ document.getElementById('createRandomGraphConfirm').addEventListener('click', fu
const nodeId = generateRealisticLabel(); const nodeId = generateRealisticLabel();
nodes.add({id: nodeId, label: nodeId, x: Math.random() * 1000, y: Math.random() * 1000}); nodes.add({id: nodeId, label: nodeId, x: Math.random() * 1000, y: Math.random() * 1000});
nodeTable[nodeId] = {'connections': [], 'packetCache': [], 'routingTable': {}}; nodeTable[nodeId] = {'connections': [], 'packetCache': [], 'routingTable': {}};
onNodeAdd(); onNodeAdd(nodeId);
nodeCounter++; nodeCounter++;
if (nodeCounter >= nodeCount) { if (nodeCounter >= nodeCount) {
clearInterval(interval); clearInterval(interval);
@@ -895,6 +934,11 @@ document.getElementById('showGrid').addEventListener('change', function() {
network.redraw(); network.redraw();
}); });
document.getElementById('showNodeUpdates').addEventListener('change', function() {
console.log(this.checked);
showUpdates = this.checked;
});
function consoleLog(message) { function consoleLog(message) {
const consoleOutput = document.getElementById('consoleOutput'); const consoleOutput = document.getElementById('consoleOutput');
consoleOutput.innerHTML = `${message}`; consoleOutput.innerHTML = `${message}`;