upload file simulasi pertama
This commit is contained in:
700
sim_smart_traffic.html
Normal file
700
sim_smart_traffic.html
Normal 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>
|
||||||
Reference in New Issue
Block a user