tsx import React, { useState, useEffect, useCallback, useRef } from 'react'; interface Node { id: string; x: number; y: number; type: 'reservoir' | 'treatment' | 'neighborhood'; pressure: number; demand: number; capacity?: number; online?: boolean; flowIn: number; flowOut: number; } interface Pipe { id: string; from: string; to: string; flow: number; capacity: number; burst: boolean; } const INITIAL_RESERVOIR_LEVEL = 100; const SIMULATION_SPEED = 50; const PIPE_BREAK_RATE = 0.02; export default function WaterSystemSimulation() { const [time, setTime] = useState(0); const [reservoirLevel, setReservoirLevel] = useState(INITIAL_RESERVOIR_LEVEL); const [nodes, setNodes] = useState([]); const [pipes, setPipes] = useState([]); const [activeEvent, setActiveEvent] = useState(null); const [isPaused, setIsPaused] = useState(false); const canvasRef = useRef(null); // Initialize network const initializeNetwork = useCallback(() => { const reservoir: Node = { id: 'reservoir', x: 400, y: 60, type: 'reservoir', pressure: 100, demand: 0, capacity: 500, flowIn: 0, flowOut: 0, }; const treatments: Node[] = [ { id: 't1', x: 200, y: 180, type: 'treatment', pressure: 90, demand: 0, capacity: 150, online: true, flowIn: 0, flowOut: 0 }, { id: 't2', x: 400, y: 180, type: 'treatment', pressure: 90, demand: 0, capacity: 150, online: true, flowIn: 0, flowOut: 0 }, { id: 't3', x: 600, y: 180, type: 'treatment', pressure: 90, demand: 0, capacity: 150, online: true, flowIn: 0, flowOut: 0 }, ]; const neighborhoods: Node[] = []; for (let i = 0; i < 20; i++) { const row = Math.floor(i / 5); const col = i % 5; const baseDemand = 5 + Math.random() * 10; neighborhoods.push({ id: `n${i}`, x: 100 + col * 150 + (row % 2) * 75, y: 300 + row * 70, type: 'neighborhood', pressure: 70 + Math.random() * 20, demand: baseDemand, flowIn: 0, flowOut: baseDemand, }); } const allNodes = [reservoir, ...treatments, ...neighborhoods]; const initialPipes: Pipe[] = []; // Reservoir to treatments initialPipes.push({ id: 'p-r-t1', from: 'reservoir', to: 't1', flow: 0, capacity: 100, burst: false }); initialPipes.push({ id: 'p-r-t2', from: 'reservoir', to: 't2', flow: 0, capacity: 100, burst: false }); initialPipes.push({ id: 'p-r-t3', from: 'reservoir', to: 't3', flow: 0, capacity: 100, burst: false }); // Treatments to neighborhoods const treatmentNeighbors: { [key: string]: number[] } = { t1: [0, 1, 2, 3, 4, 5], t2: [6, 7, 8, 9, 10, 11, 12], t3: [13, 14, 15, 16, 17, 18, 19], }; Object.entries(treatmentNeighbors).forEach(([tId, nIds]) => { nIds.forEach(nId => { initialPipes.push({ id: `p-${tId}-n${nId}`, from: tId, to: `n${nId}`, flow: 0, capacity: 40, burst: false }); }); }); setNodes(allNodes); setPipes(initialPipes); }, []); useEffect(() => { initializeNetwork(); }, [initializeNetwork]); // Calculate demand based on time of day const getDemandMultiplier = useCallback((hour: number, event: string | null) => { let multiplier = 1; // Morning shower peak (6-9 AM) if (hour >= 6 && hour < 9) { multiplier = 2.5; } // Midday business usage (11-14) else if (hour >= 11 && hour < 14) { multiplier = 1.8; } // Evening peak (18-21) else if (hour >= 18 && hour < 21) { multiplier = 2.2; } // Night low (22-5) else { multiplier = 0.5; } // Heatwave event doubles demand if (event === 'heatwave') { multiplier *= 2; } return multiplier; }, []); // Simulate water flow const simulateStep = useCallback(() => { const hour = (time / 60) % 24; const demandMultiplier = getDemandMultiplier(hour, activeEvent); setNodes(prevNodes => { const newNodes = [...prevNodes]; // Calculate total demand let totalDemand = 0; newNodes.forEach((node, idx) => { if (node.type === 'neighborhood') { const newDemand = (5 + idx * 0.5) * demandMultiplier; totalDemand += newDemand; newNodes[idx] = { ...node, demand: newDemand, flowOut: newDemand }; } }); // Calculate available supply from treatments let availableSupply = 0; newNodes.forEach((node, idx) => { if (node.type === 'treatment' && node.online !== false) { const supply = node.capacity! * (0.7 + Math.random() * 0.3); availableSupply += supply; newNodes[idx] = { ...node, flowOut: supply }; } }); // Calculate supply deficit const supplyDeficit = Math.max(0, totalDemand - availableSupply - reservoirLevel * 2); // Update reservoir level setReservoirLevel(prev => Math.max(0, prev - supplyDeficit / 100 + 0.5)); // Update pressures based on supply/demand balance newNodes.forEach((node, idx) => { if (node.type === 'neighborhood') { const supplyRatio = Math.min(1, (availableSupply * 0.15) / node.demand); const basePressure = supplyRatio * 80 + (node.online !== false ? 20 : 0); newNodes[idx] = { ...node, pressure: basePressure, flowIn: node.demand * supplyRatio }; } if (node.type === 'treatment') { const nodeDemand = newNodes.filter(n => n.id.startsWith('n')) .reduce((sum, n) => sum + n.demand / 7, 0); const treatmentIdx = parseInt(node.id.replace('t', '')) - 1; const assignedDemand = [6, 7, 7][treatmentIdx] * demandMultiplier; const pressure = node.online !== false ? 85 + Math.random() * 10 : 0; newNodes[idx] = { ...node, pressure: node.online !== false ? 85 + Math.random() * 10 : 0 }; } }); return newNodes; }); // Update pipe flows setPipes(prevPipes => { return prevPipes.map(pipe => { if (pipe.burst) return pipe; // Random chance of pipe burst if (activeEvent === 'pipeburst' && Math.random() < PIPE_BREAK_RATE) { return { ...pipe, burst: true, flow: 0 }; } // Calculate flow based on connected nodes const fromNode = nodes.find(n => n.id === pipe.from); if (fromNode && fromNode.online !== false) { const newFlow = Math.min(pipe.capacity, fromNode.flowOut * 0.3); return { ...pipe, flow: newFlow }; } return pipe; }); }); }, [time, activeEvent, nodes, getDemandMultiplier]); // Handle treatment plant offline const toggleTreatmentPlant = useCallback((plantId: string) => { setNodes(prevNodes => prevNodes.map(node => { if (node.id === plantId && node.type === 'treatment') { return { ...node, online: !node.online }; } return node; }) ); }, []); // Main simulation loop useEffect(() => { if (isPaused) return; const interval = setInterval(() => { setTime(prev => prev + 1); simulateStep(); }, SIMULATION_SPEED); return () => clearInterval(interval); }, [isPaused, simulateStep]); // Render canvas useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; // Clear canvas ctx.fillStyle = '#1a1a2e'; ctx.fillRect(0, 0, 800, 500); // Draw grid background ctx.strokeStyle = '#2a2a4e'; ctx.lineWidth = 0.5; for (let x = 0; x < 800; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, 500); ctx.stroke(); } for (let y = 0; y < 500; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(800, y); ctx.stroke(); } // Draw pipes pipes.forEach(pipe => { const fromNode = nodes.find(n => n.id === pipe.from); const toNode = nodes.find(n => n.id === pipe.to); if (!fromNode || !toNode) return; ctx.beginPath(); ctx.moveTo(fromNode.x, fromNode.y); ctx.lineTo(toNode.x, toNode.y); if (pipe.burst) { ctx.strokeStyle = '#ef4444'; ctx.lineWidth = 4; ctx.setLineDash([10, 5]); } else { const flowRatio = pipe.flow / pipe.capacity; const thickness = 2 + flowRatio * 8; // Color based on flow if (flowRatio > 0.8) { ctx.strokeStyle = '#3b82f6'; } else if (flowRatio > 0.5) { ctx.strokeStyle = '#22c55e'; } else if (flowRatio > 0.2) { ctx.strokeStyle = '#eab308'; } else { ctx.strokeStyle = '#ef4444'; } ctx.lineWidth = thickness; ctx.setLineDash([]); } ctx.stroke(); ctx.setLineDash([]); // Draw burst indicator if (pipe.burst) { ctx.fillStyle = '#ef4444'; ctx.beginPath(); ctx.arc((fromNode.x + toNode.x) / 2, (fromNode.y + toNode.y) / 2, 8, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = 'bold 12px sans-serif'; ctx.fillText('⚠', (fromNode.x + toNode.x) / 2 - 5, (fromNode.y + toNode.y) / 2 + 4); } }); // Draw nodes nodes.forEach(node => { // Draw node background const radius = node.type === 'reservoir' ? 35 : node.type === 'treatment' ? 25 : 18; let nodeColor = '#3b82f6'; if (node.type === 'neighborhood') { if (node.pressure > 70) nodeColor = '#22c55e'; else if (node.pressure > 40) nodeColor = '#eab308'; else nodeColor = '#ef4444'; } if (node.type === 'treatment' && node.online === false) { nodeColor = '#6b7280'; } if (node.type === 'reservoir') { nodeColor = '#06b6d4'; } ctx.beginPath(); ctx.arc(node.x, node.y, radius, 0, Math.PI * 2); ctx.fillStyle = nodeColor; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke(); // Draw labels ctx.fillStyle = '#fff'; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'center'; if (node.type === 'reservoir') { ctx.fillText('RESERVOIR', node.x, node.y + 50); // Draw level bar const levelWidth = 60; const levelHeight = 10; ctx.fillStyle = '#1a1a2e'; ctx.fillRect(node.x - levelWidth/2, node.y + 35, levelWidth, levelHeight); ctx.fillStyle = '#06b6d4'; ctx.fillRect(node.x - levelWidth/2, node.y + 35, levelWidth * (reservoirLevel / 100), levelHeight); } else if (node.type === 'treatment') { const label = `T${node.id.replace('t', '')}`; ctx.fillText(label, node.x, node.y + 4); if (node.online === false) { ctx.fillStyle = '#ef4444'; ctx.fillText('OFFLINE', node.x, node.y + 40); } } else { // Neighborhood pressure gauge const nId = parseInt(node.id.replace('n', '')) + 1; ctx.fillText(`${Math.round(node.pressure)}`, node.x, node.y + 4); // Small pressure bar const barWidth = 20; const barHeight = 3; ctx.fillStyle = '#1a1a2e'; ctx.fillRect(node.x - barWidth/2, node.y + 15, barWidth, barHeight); ctx.fillStyle = nodeColor; ctx.fillRect(node.x - barWidth/2, node.y + 15, barWidth * (node.pressure / 100), barHeight); } }); // Draw flow indicators on pipes pipes.forEach(pipe => { if (pipe.flow <= 0 || pipe.burst) return; const fromNode = nodes.find(n => n.id === pipe.from); const toNode = nodes.find(n => n.id === pipe.to); if (!fromNode || !toNode) return; const midX = (fromNode.x + toNode.x) / 2; const midY = (fromNode.y + toNode.y) / 2; // Animated flow particles const particleOffset = (time * 0.1) % 20; const dx = toNode.x - fromNode.x; const dy = toNode.y - fromNode.y; const len = Math.sqrt(dx * dx + dy * dy); const nx = dx / len; const ny = dy / len; for (let i = 0; i < 3; i++) { const t = ((i * 0.33 + particleOffset / 20) % 1); const px = fromNode.x + dx * t; const py = fromNode.y + dy * t; ctx.beginPath(); ctx.arc(px, py, 3, 0, Math.PI * 2); ctx.fillStyle = 'rgba(59, 130, 246, 0.8)'; ctx.fill(); } }); }, [nodes, pipes, time, reservoirLevel]); const hour = Math.floor((time / 60) % 24); const minute = Math.floor(time % 60); const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; const demandMultiplier = getDemandMultiplier(hour, activeEvent); return (

City Water Distribution Simulation

Real-time water network monitoring and control

{/* Main Canvas */}
Time: {timeStr}
Demand: {demandMultiplier.toFixed(1)}x
Reservoir: 30 ? 'text-cyan-400' : 'text-red-400'}`}> {Math.round(reservoirLevel)}%
{/* Legend */}
Reservoir
Treatment Plant
High Pressure (>70)
Medium Pressure (40-70)
Low Pressure (<40)
{/* Control Panel */}
{/* Events */}

Trigger Events

{/* Treatment Plant Status */}

Treatment Plants

{['t1', 't2', 't3'].map(id => { const plant = nodes.find(n => n.id === id); return (
Plant {id.replace('t', '')}
); })}
{/* System Stats */}

System Statistics

Total Demand: {nodes.filter(n => n.type === 'neighborhood').reduce((sum, n) => sum + n.demand, 0).toFixed(1)} units
Supply Capacity: {nodes.filter(n => n.type === 'treatment').filter(n => n.online !== false).reduce((sum, n) => sum + (n.capacity || 0), 0)} units
Active Pipes: {pipes.filter(p => !p.burst).length}/{pipes.length}
Critical Zones: n.type === 'neighborhood' && n.pressure < 40).length > 0 ? 'text-red-400' : 'text-green-400' }`}> {nodes.filter(n => n.type === 'neighborhood' && n.pressure < 40).length}
{/* Neighborhood Pressure Grid */}

Neighborhood Pressures

{nodes.filter(n => n.type === 'neighborhood').map((n, i) => (
70 ? 'bg-green-900 text-green-300' : n.pressure > 40 ? 'bg-yellow-900 text-yellow-300' : 'bg-red-900 text-red-300' }`} > {i + 1}: {Math.round(n.pressure)}
))}
{/* Status Messages */} {activeEvent === 'heatwave' && (
⚠️ HEATWAVE ALERT: Water demand has doubled. Reservoir levels dropping faster than normal.
)} {activeEvent === 'pipeburst' && (
💥 PIPE BURST WARNING: Random pipe failures occurring. Monitor affected neighborhoods.
)} {nodes.filter(n => n.type === 'neighborhood' && n.pressure < 30).length > 0 && (
🚨 CRITICAL: Neighborhoods experiencing complete water loss. Immediate intervention required!
)} {reservoirLevel < 20 && (
🔴 RESERVOIR CRITICAL: Water reserves below 20%. System at risk of complete failure.
)}
); }