function calculateCoverageGrid(sensors, radius, gridSize, cellSize) {
const coverageCount = new Array(gridSize * gridSize).fill(0);
for (let y = 0; y < gridSize; y++) {
for (let x = 0; x < gridSize; x++) {
const px = (x + 0.5) * cellSize;
const py = (y + 0.5) * cellSize;
for (const sensor of sensors) {
const dx = px - sensor.x;
const dy = py - sensor.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= radius * cellSize) {
coverageCount[y * gridSize + x]++;
}
}
}
}
return coverageCount;
}
function calculateMetrics(coverageCount, kLevel) {
const totalCells = coverageCount.length;
let coveredCells = 0;
let kCoveredCells = 0;
let redundantCells = 0;
let holeCells = 0;
for (const count of coverageCount) {
if (count > 0) coveredCells++;
if (count >= kLevel) kCoveredCells++;
if (count > 1) redundantCells++;
if (count === 0) holeCells++;
}
return {
totalCoverage: ((coveredCells / totalCells) * 100).toFixed(1),
kCoverage: ((kCoveredCells / totalCells) * 100).toFixed(1),
redundancy: ((redundantCells / totalCells) * 100).toFixed(1),
holes: ((holeCells / totalCells) * 100).toFixed(1),
sensorCount: sensors.length
};
}
// Calculate coverage grid
coverageGrid = calculateCoverageGrid(sensors, sensingRadius, gridSize, cellSize)
metrics = calculateMetrics(coverageGrid, kCoverageLevel)
// Color functions
function getCoverageColor(count, kLevel, showHoles, showRedund) {
if (count === 0) {
return showHoles ? "rgba(231, 76, 60, 0.6)" : "rgba(236, 240, 241, 0.3)";
}
if (count >= kLevel) {
if (count > kLevel && showRedund) {
// Gradient for redundancy: more sensors = darker teal
const intensity = Math.min(count / 4, 1);
return `rgba(22, 160, 133, ${0.3 + intensity * 0.5})`;
}
return "rgba(22, 160, 133, 0.4)"; // k-covered: teal
}
// Below k-coverage: orange warning
return "rgba(230, 126, 34, 0.5)";
}
// SVG Visualization
coverageViz = {
const svg = d3.create("svg")
.attr("width", canvasWidth + 120)
.attr("height", canvasHeight + 60)
.attr("viewBox", `0 0 ${canvasWidth + 120} ${canvasHeight + 60}`)
.style("max-width", "100%")
.style("border", "2px solid #2C3E50")
.style("border-radius", "8px")
.style("background", "#ECF0F1")
.style("cursor", "crosshair");
// Main group with margin
const g = svg.append("g")
.attr("transform", "translate(10, 10)");
// Draw coverage cells
for (let y = 0; y < gridSize; y++) {
for (let x = 0; x < gridSize; x++) {
const count = coverageGrid[y * gridSize + x];
g.append("rect")
.attr("x", x * cellSize)
.attr("y", y * cellSize)
.attr("width", cellSize)
.attr("height", cellSize)
.attr("fill", getCoverageColor(count, kCoverageLevel, showCoverageHoles, showRedundancy))
.attr("stroke", "none");
}
}
// Draw grid lines (sparse)
for (let i = 0; i <= gridSize; i += 10) {
g.append("line")
.attr("x1", i * cellSize)
.attr("y1", 0)
.attr("x2", i * cellSize)
.attr("y2", canvasHeight)
.attr("stroke", "#BDC3C7")
.attr("stroke-width", 0.5);
g.append("line")
.attr("x1", 0)
.attr("y1", i * cellSize)
.attr("x2", canvasWidth)
.attr("y2", i * cellSize)
.attr("stroke", "#BDC3C7")
.attr("stroke-width", 0.5);
}
// Draw sensor coverage circles
for (const sensor of sensors) {
g.append("circle")
.attr("cx", sensor.x)
.attr("cy", sensor.y)
.attr("r", sensingRadius * cellSize)
.attr("fill", "rgba(22, 160, 133, 0.15)")
.attr("stroke", "#16A085")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,3");
}
// Draw sensor nodes
for (const sensor of sensors) {
g.append("circle")
.attr("cx", sensor.x)
.attr("cy", sensor.y)
.attr("r", 8)
.attr("fill", "#2C3E50")
.attr("stroke", "#16A085")
.attr("stroke-width", 3);
// Sensor ID label
g.append("text")
.attr("x", sensor.x)
.attr("y", sensor.y + 3)
.attr("text-anchor", "middle")
.attr("fill", "white")
.attr("font-size", "8px")
.attr("font-weight", "bold")
.text(sensors.indexOf(sensor) + 1);
}
// Legend
const legend = svg.append("g")
.attr("transform", `translate(${canvasWidth + 20}, 20)`);
legend.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("fill", "#2C3E50")
.text("Legend");
const legendItems = [
{ color: "rgba(22, 160, 133, 0.4)", label: `${kCoverageLevel}-covered` },
{ color: "rgba(22, 160, 133, 0.8)", label: "Redundant" },
{ color: "rgba(230, 126, 34, 0.5)", label: `< ${kCoverageLevel}-coverage` },
{ color: "rgba(231, 76, 60, 0.6)", label: "Hole" },
{ color: "#2C3E50", label: "Sensor" }
];
legendItems.forEach((item, i) => {
if (item.label === "Sensor") {
legend.append("circle")
.attr("cx", 8)
.attr("cy", 25 + i * 22)
.attr("r", 6)
.attr("fill", item.color)
.attr("stroke", "#16A085")
.attr("stroke-width", 2);
} else {
legend.append("rect")
.attr("x", 0)
.attr("y", 17 + i * 22)
.attr("width", 16)
.attr("height", 16)
.attr("fill", item.color)
.attr("stroke", "#2C3E50")
.attr("stroke-width", 1);
}
legend.append("text")
.attr("x", 22)
.attr("y", 28 + i * 22)
.attr("font-size", "10px")
.attr("fill", "#2C3E50")
.text(item.label);
});
// Click handler for adding/removing sensors
svg.on("click", function(event) {
const [mx, my] = d3.pointer(event);
const x = mx - 10;
const y = my - 10;
// Check if within grid bounds
if (x < 0 || x > canvasWidth || y < 0 || y > canvasHeight) return;
// Check if clicking on existing sensor (to remove)
const clickRadius = 15;
const existingIndex = sensors.findIndex(s => {
const dx = s.x - x;
const dy = s.y - y;
return Math.sqrt(dx * dx + dy * dy) < clickRadius;
});
if (existingIndex >= 0) {
// Remove sensor
mutable sensors = sensors.filter((_, i) => i !== existingIndex);
} else {
// Add new sensor
mutable sensors = [...sensors, { x, y }];
}
});
return svg.node();
}
// Display the visualization
coverageViz