upload file simulasi pertama

This commit is contained in:
2026-03-05 11:12:29 +07:00
commit 4d2e403ead

700
sim_smart_traffic.html Normal file
View File

@@ -0,0 +1,700 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IoT Smart Traffic AI Simulation</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root {
--road-color: #333;
--line-color: #fff;
}
body {
background-color: #1a202c;
color: #fff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow-x: hidden;
}
/* Traffic Lights */
.light {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #444;
margin: 4px auto;
transition: background-color 0.3s;
}
.light.red.active {
background-color: #ff3e3e;
box-shadow: 0 0 15px #ff3e3e;
}
.light.yellow.active {
background-color: #ffcc00;
box-shadow: 0 0 15px #ffcc00;
}
.light.green.active {
background-color: #2ecc71;
box-shadow: 0 0 15px #2ecc71;
}
/* Simulation Canvas Area */
#simulation-container {
position: relative;
width: 100%;
height: 600px;
background-color: #2d3748;
border-radius: 12px;
overflow: hidden;
border: 2px solid #4a5568;
}
canvas {
display: block;
}
/* AI Overlay Styles */
.ai-label {
position: absolute;
border: 2px solid #00f2ff;
color: #00f2ff;
font-size: 10px;
font-weight: bold;
pointer-events: none;
background: rgba(0, 242, 255, 0.1);
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 9999px;
font-size: 12px;
}
.online {
background-color: #065f46;
color: #34d399;
}
.sidebar {
background: rgba(26, 32, 44, 0.9);
backdrop-filter: blur(10px);
}
.log-container {
height: 150px;
overflow-y: auto;
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
background: #000;
padding: 10px;
border-radius: 4px;
}
.vehicle-count-card {
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
border-left: 4px solid #4299e1;
}
</style>
</head>
<body class="p-4 md:p-8">
<div class="max-w-7xl mx-auto">
<header class="flex justify-between items-center mb-6">
<div>
<h1 class="text-3xl font-bold text-blue-400">Smart Traffic IoT Dashboard</h1>
<p class="text-gray-400">Monitoring AI & Edge Computing IP Camera</p>
</div>
<div class="text-right">
<div class="status-badge online mb-1">
<span class="w-2 h-2 bg-green-400 rounded-full mr-2"></span> System Online
</div>
<p id="clock" class="text-sm font-mono"></p>
</div>
</header>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<!-- Left Panel: Stats -->
<div class="lg:col-span-1 space-y-4">
<div class="vehicle-count-card p-4 rounded-lg shadow-lg">
<h3 class="text-gray-400 text-sm uppercase">Deteksi Kendaraan</h3>
<div class="flex justify-between items-end mt-2">
<span id="total-vehicles" class="text-4xl font-bold text-white">0</span>
<span class="text-blue-400 text-sm">Units/Min</span>
</div>
</div>
<div class="bg-gray-800 p-4 rounded-lg shadow-lg">
<h3 class="text-gray-400 text-sm mb-3">IP Camera Feed (AI Processed)</h3>
<div class="space-y-3">
<div class="flex items-center justify-between text-xs">
<span>CAM_NORTH_01</span>
<span class="text-green-400">● ACTIVE</span>
</div>
<div class="flex items-center justify-between text-xs text-gray-400">
<span>Resolution</span>
<span>1920x1080 (30fps)</span>
</div>
<div class="flex items-center justify-between text-xs text-gray-400">
<span>AI Latency</span>
<span id="ai-latency">12ms</span>
</div>
</div>
</div>
<div class="bg-gray-800 p-4 rounded-lg shadow-lg">
<h3 class="text-gray-400 text-sm mb-3">Traffic Control Mode</h3>
<select id="control-mode"
class="w-full bg-gray-700 border border-gray-600 text-sm rounded p-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-white cursor-pointer">
<option value="adaptive">AI Adaptive Timing</option>
<option value="fixed">Fixed Schedule (10s)</option>
<option value="manual">Manual Override</option>
</select>
</div>
<div class="bg-gray-800 p-4 rounded-lg shadow-lg">
<h3 class="text-gray-400 text-sm mb-3">Traffic Simulation Control</h3>
<div class="flex space-x-2">
<button id="start-btn"
class="flex-1 bg-green-600 hover:bg-green-500 text-white font-bold py-2 px-4 rounded-lg transition-all duration-300 flex items-center justify-center shadow-lg shadow-green-900/20 active:scale-95">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
clip-rule="evenodd"></path>
</svg>
Start
</button>
<button id="stop-btn"
class="flex-1 bg-gray-600 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg transition-all duration-300 flex items-center justify-center shadow-lg active:scale-95 opacity-50 cursor-not-allowed"
disabled>
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z"
clip-rule="evenodd"></path>
</svg>
Stop
</button>
</div>
</div>
<div class="bg-gray-800 p-4 rounded-lg shadow-lg">
<h3 class="text-gray-400 text-sm mb-2">System Logs</h3>
<div id="logs" class="log-container text-blue-300">
<div>[INFO] System waiting for user command...</div>
</div>
</div>
</div>
<!-- Main Panel: Simulation -->
<div class="lg:col-span-3">
<div id="simulation-container">
<canvas id="trafficCanvas"></canvas>
<!-- Traffic Lights Overlay UI -->
<!-- North -->
<div
class="absolute top-10 left-1/2 -translate-x-12 bg-black/80 backdrop-blur-sm p-1 rounded-lg border border-gray-700 shadow-2xl">
<div id="light-north-red" class="light red active"></div>
<div id="light-north-yellow" class="light yellow"></div>
<div id="light-north-green" class="light green"></div>
</div>
<!-- South -->
<div
class="absolute bottom-10 left-1/2 translate-x-4 bg-black/80 backdrop-blur-sm p-1 rounded-lg border border-gray-700 shadow-2xl">
<div id="light-south-green" class="light green"></div>
<div id="light-south-yellow" class="light yellow"></div>
<div id="light-south-red" class="light red active"></div>
</div>
<!-- East -->
<div
class="absolute top-1/2 -translate-y-12 right-10 bg-black/80 backdrop-blur-sm p-1 rounded-lg border border-gray-700 shadow-2xl flex">
<div id="light-east-green" class="light green"></div>
<div id="light-east-yellow" class="light yellow"></div>
<div id="light-east-red" class="light red active"></div>
</div>
<!-- West -->
<div
class="absolute top-1/2 translate-y-4 left-10 bg-black/80 backdrop-blur-sm p-1 rounded-lg border border-gray-700 shadow-2xl flex">
<div id="light-west-red" class="light red active"></div>
<div id="light-west-yellow" class="light yellow"></div>
<div id="light-west-green" class="light green"></div>
</div>
<div id="ai-overlay"></div>
<div
class="absolute bottom-4 right-4 bg-black/60 p-2 rounded text-[10px] uppercase font-bold tracking-widest text-white border border-white/10 backdrop-blur-sm">
Live Simulation - AI Visual Node
</div>
</div>
<div class="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4">
<div
class="bg-gray-800 p-3 rounded-xl border-b-4 border-red-500 shadow-lg transform transition-transform hover:scale-105 hover:bg-gray-750">
<div class="text-[10px] text-gray-400 uppercase font-semibold">Arah Utara</div>
<div class="text-lg font-black text-white" id="density-north">IDLE</div>
</div>
<div
class="bg-gray-800 p-3 rounded-xl border-b-4 border-green-500 shadow-lg transform transition-transform hover:scale-105 hover:bg-gray-750">
<div class="text-[10px] text-gray-400 uppercase font-semibold">Arah Selatan</div>
<div class="text-lg font-black text-white" id="density-south">IDLE</div>
</div>
<div
class="bg-gray-800 p-3 rounded-xl border-b-4 border-blue-500 shadow-lg transform transition-transform hover:scale-105 hover:bg-gray-750">
<div class="text-[10px] text-gray-400 uppercase font-semibold">Arah Timur</div>
<div class="text-lg font-black text-white" id="density-east">IDLE</div>
</div>
<div
class="bg-gray-800 p-3 rounded-xl border-b-4 border-yellow-500 shadow-lg transform transition-transform hover:scale-105 hover:bg-gray-750">
<div class="text-[10px] text-gray-400 uppercase font-semibold">Arah Barat</div>
<div class="text-lg font-black text-white" id="density-west">IDLE</div>
</div>
</div>
</div>
</div>
</div>
<script>
const canvas = document.getElementById('trafficCanvas');
const ctx = canvas.getContext('2d');
const aiOverlay = document.getElementById('ai-overlay');
const logsContainer = document.getElementById('logs');
const startBtn = document.getElementById('start-btn');
const stopBtn = document.getElementById('stop-btn');
// Simulation Constants
const ROAD_WIDTH = 120;
const VEHICLE_TYPES = [
{ type: 'car', color: '#3498db', width: 25, height: 45 },
{ type: 'car', color: '#e74c3c', width: 25, height: 45 },
{ type: 'motorcycle', color: '#f1c40f', width: 12, height: 28 },
{ type: 'truck', color: '#95a5a6', width: 32, height: 70 }
];
let vehicles = [];
let lights = {
northSouth: 'red',
eastWest: 'green'
};
let timer = 0;
let isRunning = false;
let animationFrameId = null;
// Adaptive AI & Traffic Simulation Variables
let currentCycleDuration = 400;
let activePhase = 'EW';
// Dynamic Traffic Scenario (Waves)
let trafficRates = { 'N': 0.02, 'S': 0.02, 'E': 0.02, 'W': 0.02 };
function updateTrafficScenario() {
const levels = [0.005, 0.02, 0.05, 0.08]; // Low, Med, High, Rush
['N', 'S', 'E', 'W'].forEach(dir => {
trafficRates[dir] = levels[Math.floor(Math.random() * levels.length)];
});
addLog("System: Traffic flow scenario updated (Random Density)");
}
setInterval(updateTrafficScenario, 15000); // Change every 15s
function addLog(msg) {
const div = document.createElement('div');
div.className = 'border-l-2 border-blue-500 pl-2 mb-1';
div.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
logsContainer.prepend(div);
if (logsContainer.children.length > 20) logsContainer.lastChild.remove();
}
function resize() {
canvas.width = canvas.parentElement.clientWidth;
canvas.height = canvas.parentElement.clientHeight;
drawRoads();
}
window.addEventListener('resize', resize);
resize();
class Vehicle {
constructor(direction) {
this.id = Math.random().toString(36).substr(2, 9);
this.direction = direction; // 'N', 'S', 'E', 'W'
const config = VEHICLE_TYPES[Math.floor(Math.random() * VEHICLE_TYPES.length)];
this.type = config.type;
this.color = config.color;
this.w = (direction === 'E' || direction === 'W') ? config.height : config.width;
this.h = (direction === 'E' || direction === 'W') ? config.width : config.height;
this.speed = 1.5 + Math.random();
this.originalSpeed = this.speed;
this.stopped = false;
// Set initial position
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
if (direction === 'N') {
this.x = centerX - ROAD_WIDTH / 4 - this.w / 2;
this.y = -100;
} else if (direction === 'S') {
this.x = centerX + ROAD_WIDTH / 4 - this.w / 2;
this.y = canvas.height + 100;
} else if (direction === 'E') {
this.x = canvas.width + 100;
this.y = centerY - ROAD_WIDTH / 4 - this.h / 2;
} else if (direction === 'W') {
this.x = -100;
this.y = centerY + ROAD_WIDTH / 4 - this.h / 2;
}
}
draw() {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.w, this.h);
// Windshield and details
ctx.fillStyle = 'rgba(255,255,255,0.3)';
if (this.direction === 'N' || this.direction === 'S') {
ctx.fillRect(this.x + 2, this.y + (this.direction === 'N' ? this.h - 12 : 5), this.w - 4, 8);
} else {
ctx.fillRect(this.x + (this.direction === 'W' ? this.w - 12 : 5), this.y + 2, 8, this.h - 4);
}
// AI Bounding Box Logic (Simulated)
this.renderAIOverlay();
}
renderAIOverlay() {
let overlay = document.getElementById(`ai-${this.id}`);
if (!overlay) {
overlay = document.createElement('div');
overlay.id = `ai-${this.id}`;
overlay.className = 'ai-label';
aiOverlay.appendChild(overlay);
}
overlay.style.width = `${this.w}px`;
overlay.style.height = `${this.h}px`;
overlay.style.left = `${this.x}px`;
overlay.style.top = `${this.y}px`;
overlay.innerHTML = `<span>${this.type.toUpperCase()}</span>`;
// Remove if out of bounds
if (this.isOutOfBounds()) {
overlay.remove();
}
}
update() {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const stopLineBuffer = 85;
const intersectionSize = ROAD_WIDTH / 2 + 10;
let shouldStop = false;
// 1. Traffic Light Stop Logic
const isAtStopLine = (
(this.direction === 'N' && this.y < centerY - stopLineBuffer && this.y > centerY - stopLineBuffer - 40) ||
(this.direction === 'S' && this.y > centerY + stopLineBuffer - this.h && this.y < centerY + stopLineBuffer - this.h + 40) ||
(this.direction === 'E' && this.x > centerX + stopLineBuffer - this.w && this.x < centerX + stopLineBuffer - this.w + 40) ||
(this.direction === 'W' && this.x < centerX - stopLineBuffer && this.x > centerX - stopLineBuffer - 40)
);
const currentLight = (this.direction === 'N' || this.direction === 'S') ? lights.northSouth : lights.eastWest;
if (isAtStopLine && (currentLight === 'red' || currentLight === 'yellow')) {
shouldStop = true;
}
// 2. Advanced Collision Avoidance (Same Lane)
// We check for ALL vehicles in front of us, not just direct lane
vehicles.forEach(other => {
if (other === this) return;
if (this.direction === other.direction) {
const dist = this.getDistance(other);
// Dynamic safety gap based on speed and vehicle length
const minGap = 25 + (this.speed * 10);
if (dist > 0 && dist < minGap) {
shouldStop = true;
}
}
});
// 3. Gridlock Prevention (Don't enter intersection if exit is blocked)
const isEnteringIntersection = isAtStopLine && currentLight === 'green';
if (isEnteringIntersection) {
const carInFrontNearExit = vehicles.some(other => {
if (other === this || other.direction !== this.direction) return false;
const dist = this.getDistance(other);
return dist > 0 && dist < (ROAD_WIDTH + 40); // Check if we can clear the intersection
});
if (carInFrontNearExit) shouldStop = true;
}
// Physics Simulation: Smooth Braking & Acceleration
if (shouldStop) {
this.speed = Math.max(0, this.speed - 0.25);
} else {
// Constant speed when in intersection to avoid getting stuck
const inIntersection = (this.x > centerX - intersectionSize && this.x < centerX + intersectionSize &&
this.y > centerY - intersectionSize && this.y < centerY + intersectionSize);
const targetSpeed = inIntersection ? Math.max(2, this.originalSpeed) : this.originalSpeed;
this.speed = Math.min(targetSpeed, this.speed + 0.15);
}
if (this.direction === 'N') this.y += this.speed;
if (this.direction === 'S') this.y -= this.speed;
if (this.direction === 'E') this.x -= this.speed;
if (this.direction === 'W') this.x += this.speed;
}
getDistance(other) {
// Returns distance between front of 'this' and back of 'other'
if (this.direction === 'N') return other.y - (this.y + this.h);
if (this.direction === 'S') return this.y - (other.y + other.h);
if (this.direction === 'E') return this.x - (other.x + other.w);
if (this.direction === 'W') return other.x - (this.x + this.w);
return 1000;
}
isOutOfBounds() {
return (this.x < -150 || this.x > canvas.width + 150 || this.y < -150 || this.y > canvas.height + 150);
}
removeOverlay() {
const overlay = document.getElementById(`ai-${this.id}`);
if (overlay) overlay.remove();
}
}
function drawRoads() {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// Main Background
ctx.fillStyle = '#2d3748';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Roads
ctx.fillStyle = '#333';
// Vertical Road
ctx.fillRect(centerX - ROAD_WIDTH / 2, 0, ROAD_WIDTH, canvas.height);
// Horizontal Road
ctx.fillRect(0, centerY - ROAD_WIDTH / 2, canvas.width, ROAD_WIDTH);
// Lines
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.setLineDash([20, 20]);
// Vertical Center Line
ctx.beginPath();
ctx.moveTo(centerX, 0);
ctx.lineTo(centerX, centerY - ROAD_WIDTH / 2);
ctx.moveTo(centerX, centerY + ROAD_WIDTH / 2);
ctx.lineTo(centerX, canvas.height);
ctx.stroke();
// Horizontal Center Line
ctx.beginPath();
ctx.moveTo(0, centerY);
ctx.lineTo(centerX - ROAD_WIDTH / 2, centerY);
ctx.moveTo(centerX + ROAD_WIDTH / 2, centerY);
ctx.lineTo(canvas.width, centerY);
ctx.stroke();
// Intersection Area
ctx.fillStyle = '#444';
ctx.fillRect(centerX - ROAD_WIDTH / 2, centerY - ROAD_WIDTH / 2, ROAD_WIDTH, ROAD_WIDTH);
// Zebra Crossings
ctx.setLineDash([]);
ctx.fillStyle = '#ddd';
for (let i = 0; i < 5; i++) {
// Top
ctx.fillRect(centerX - ROAD_WIDTH / 2 + i * 25 + 2, centerY - ROAD_WIDTH / 2 - 25, 15, 20);
// Bottom
ctx.fillRect(centerX - ROAD_WIDTH / 2 + i * 25 + 2, centerY + ROAD_WIDTH / 2 + 5, 15, 20);
// Left
ctx.fillRect(centerX - ROAD_WIDTH / 2 - 25, centerY - ROAD_WIDTH / 2 + i * 25 + 2, 20, 15);
// Right
ctx.fillRect(centerX + ROAD_WIDTH / 2 + 5, centerY - ROAD_WIDTH / 2 + i * 25 + 2, 20, 15);
}
}
function updateTrafficLights() {
if (!isRunning) return;
timer++;
// AI Decision: Count vehicles in each lane group
const nCount = vehicles.filter(v => v.direction === 'N' || v.direction === 'S').length;
const eCount = vehicles.filter(v => v.direction === 'E' || v.direction === 'W').length;
// Update Density UI
const setDensityColor = (id, count) => {
const el = document.getElementById(`density-${id}`);
if (count > 5) el.innerText = 'High';
else if (count > 2) el.innerText = 'Medium';
else el.innerText = 'Low';
};
setDensityColor('north', nCount);
setDensityColor('south', nCount);
setDensityColor('east', eCount);
setDensityColor('west', eCount);
// CONTROL LOGIC
const mode = document.getElementById('control-mode').value;
const yellowTime = 60;
if (mode === 'adaptive') {
// AI Logic: Duration depends on lane density
// Base 200ms + (Vehicle Count * 60ms weight)
const currentWeight = activePhase === 'NS' ? nCount : eCount;
currentCycleDuration = Math.min(1200, 250 + (currentWeight * 70));
} else if (mode === 'fixed') {
currentCycleDuration = 500; // Static 10s behavior
}
// Phase Switching Logic
if (timer >= currentCycleDuration) {
timer = 0;
if (activePhase === 'NS') {
activePhase = 'EW';
lights.northSouth = 'red';
lights.eastWest = 'green';
addLog(`AI: Switch to E-W. Density: ${eCount} units. duration: ${Math.floor(currentCycleDuration / 60)}s`);
} else {
activePhase = 'NS';
lights.northSouth = 'green';
lights.eastWest = 'red';
addLog(`AI: Switch to N-S. Density: ${nCount} units. duration: ${Math.floor(currentCycleDuration / 60)}s`);
}
} else if (timer >= currentCycleDuration - yellowTime) {
// Transition to Yellow
if (activePhase === 'NS') lights.northSouth = 'yellow';
else lights.eastWest = 'yellow';
}
// Update UI
updateLightUI('north', lights.northSouth);
updateLightUI('south', lights.northSouth);
updateLightUI('east', lights.eastWest);
updateLightUI('west', lights.eastWest);
}
function updateLightUI(id, state) {
const types = ['red', 'yellow', 'green'];
types.forEach(t => {
const el = document.getElementById(`light-${id}-${t}`);
if (t === state) el.classList.add('active');
else el.classList.remove('active');
});
}
function spawnVehicles() {
if (!isRunning) return;
const ways = ['N', 'S', 'E', 'W'];
ways.forEach(dir => {
const prob = trafficRates[dir]; // Use dynamic rates from waves
if (Math.random() < prob) {
// Robust check: ensure enough space for the LARGEST possible vehicle (truck ~70px)
const spawnBuffer = 90;
const isEntryClear = !vehicles.some(v => {
if (v.direction !== dir) return false;
if (dir === 'N') return v.y < spawnBuffer;
if (dir === 'S') return v.y > canvas.height - spawnBuffer;
if (dir === 'E') return v.x > canvas.width - spawnBuffer;
if (dir === 'W') return v.x < spawnBuffer;
return false;
});
if (isEntryClear) vehicles.push(new Vehicle(dir));
}
});
}
function animate() {
if (!isRunning) return;
drawRoads();
updateTrafficLights();
spawnVehicles();
vehicles = vehicles.filter(v => !v.isOutOfBounds());
document.getElementById('total-vehicles').innerText = vehicles.length;
document.getElementById('ai-latency').innerText = (10 + Math.floor(Math.random() * 5)) + 'ms';
vehicles.forEach(v => {
v.update();
v.draw();
});
animationFrameId = requestAnimationFrame(animate);
}
// Control Logic
startBtn.addEventListener('click', () => {
if (isRunning) return;
isRunning = true;
startBtn.classList.add('opacity-50', 'cursor-not-allowed');
startBtn.disabled = true;
stopBtn.classList.remove('opacity-50', 'cursor-not-allowed', 'bg-gray-600');
stopBtn.classList.add('bg-red-600');
stopBtn.disabled = false;
addLog("Simulation Started");
animate();
});
stopBtn.addEventListener('click', () => {
if (!isRunning) return;
isRunning = false;
cancelAnimationFrame(animationFrameId);
stopBtn.classList.add('opacity-50', 'cursor-not-allowed', 'bg-gray-600');
stopBtn.classList.remove('bg-red-600');
stopBtn.disabled = true;
startBtn.classList.remove('opacity-50', 'cursor-not-allowed');
startBtn.disabled = false;
addLog("Simulation Stopped & Reset");
// Clear state and overlays
vehicles.forEach(v => v.removeOverlay());
vehicles = [];
aiOverlay.innerHTML = '';
timer = 0;
// Reset Lights to initial state
lights = { northSouth: 'red', eastWest: 'green' };
updateLightUI('north', 'red');
updateLightUI('south', 'red');
updateLightUI('east', 'green');
updateLightUI('west', 'green');
// Reset Data UI
document.getElementById('total-vehicles').innerText = '0';
['north', 'south', 'east', 'west'].forEach(dir => {
document.getElementById(`density-${dir}`).innerText = 'IDLE';
});
drawRoads();
});
// Clock
setInterval(() => {
document.getElementById('clock').innerText = new Date().toLocaleString('id-ID');
}, 1000);
// Initial Draw
drawRoads();
</script>
</body>
</html>