import { useState, useEffect, useRef } from "react"; import { useSession, signIn } from "next-auth/react"; import Layout from "../components/ui/Layout"; import { Card, CardHeader, CardContent, CardTitle, CardDescription, Button, Alert } from "../components/ui/components"; import { BoltIcon } from '@heroicons/react/24/outline'; import axios from 'axios'; export default function Spadki() { const { data: session } = useSession(); const canvasRef = useRef(null); const [isLoading, setIsLoading] = useState(false); const [selectedTool, setSelectedTool] = useState('select'); const [selectedObject, setSelectedObject] = useState(null); const [selectedCable, setSelectedCable] = useState(null); const [objects, setObjects] = useState([]); const [cables, setCables] = useState([]); const [connectionFrom, setConnectionFrom] = useState(null); const [calculationResults, setCalculationResults] = useState(null); const [error, setError] = useState(null); const [isDragging, setIsDragging] = useState(false); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [showTooltip, setShowTooltip] = useState(false); const [tooltipContent, setTooltipContent] = useState(''); const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); // Authentication check if (!session) { return (

Wymagane logowanie

); } // Canvas setup and drawing logic useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); // Handle high-DPI displays for crisp rendering const devicePixelRatio = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); // Set actual size in memory (scaled up for high-DPI) canvas.width = rect.width * devicePixelRatio; canvas.height = rect.height * devicePixelRatio; // Scale back down using CSS canvas.style.width = rect.width + 'px'; canvas.style.height = rect.height + 'px'; // Scale the drawing context to match device pixel ratio ctx.scale(devicePixelRatio, devicePixelRatio); // Keyboard event handler const handleKeyDown = (e) => { if (e.key === 'Delete' || e.key === 'Backspace') { if (selectedObject) { deleteObject(selectedObject.id); } else if (selectedCable) { deleteCable(selectedCable.id); } } else if (e.key === 'Escape') { setSelectedObject(null); setSelectedCable(null); setConnectionFrom(null); setSelectedTool('select'); } }; // Add event listener window.addEventListener('keydown', handleKeyDown); const drawGrid = () => { const rect = canvas.getBoundingClientRect(); ctx.clearRect(0, 0, rect.width, rect.height); // Enable anti-aliasing for smoother lines ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; // Draw grid ctx.strokeStyle = '#f0f0f0'; ctx.lineWidth = 0.5; // Thinner lines for crisper appearance // Minor grid lines (20px) for (let x = 0; x <= rect.width; x += 20) { ctx.beginPath(); ctx.moveTo(x + 0.5, 0); // +0.5 for crisp 1px lines ctx.lineTo(x + 0.5, rect.height); ctx.stroke(); } for (let y = 0; y <= rect.height; y += 20) { ctx.beginPath(); ctx.moveTo(0, y + 0.5); ctx.lineTo(rect.width, y + 0.5); ctx.stroke(); } // Major grid lines (80px) ctx.strokeStyle = '#d0d0d0'; ctx.lineWidth = 1; for (let x = 0; x <= rect.width; x += 80) { ctx.beginPath(); ctx.moveTo(x + 0.5, 0); ctx.lineTo(x + 0.5, rect.height); ctx.stroke(); } for (let y = 0; y <= rect.height; y += 80) { ctx.beginPath(); ctx.moveTo(0, y + 0.5); ctx.lineTo(rect.width, y + 0.5); ctx.stroke(); } }; const drawObjects = () => { objects.forEach(obj => { const isSelected = selectedObject && selectedObject.id === obj.id; const isConnectionFrom = connectionFrom && connectionFrom.id === obj.id; ctx.save(); // Enable anti-aliasing for smooth shapes ctx.imageSmoothingEnabled = true; if (isSelected) { ctx.shadowColor = '#3b82f6'; ctx.shadowBlur = 8; } else if (isConnectionFrom) { ctx.shadowColor = '#f59e0b'; ctx.shadowBlur = 12; } switch (obj.type) { case 'transformer': // Draw transformer as green triangle ctx.fillStyle = isConnectionFrom ? '#f59e0b' : '#10b981'; ctx.strokeStyle = isConnectionFrom ? '#d97706' : '#059669'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(obj.x, obj.y - 15); ctx.lineTo(obj.x - 15, obj.y + 15); ctx.lineTo(obj.x + 15, obj.y + 15); ctx.closePath(); ctx.fill(); ctx.stroke(); break; case 'box': // Draw cable box as blue square ctx.fillStyle = isConnectionFrom ? '#f59e0b' : '#3b82f6'; ctx.strokeStyle = isConnectionFrom ? '#d97706' : '#1d4ed8'; ctx.lineWidth = 1.5; ctx.fillRect(obj.x - 15, obj.y - 15, 30, 30); ctx.strokeRect(obj.x - 15, obj.y - 15, 30, 30); // Draw box symbol ctx.fillStyle = '#ffffff'; ctx.font = '11px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('⬛', obj.x, obj.y); break; case 'pole': // Draw pole as dark gray square ctx.fillStyle = isConnectionFrom ? '#f59e0b' : '#4b5563'; ctx.strokeStyle = isConnectionFrom ? '#d97706' : '#374151'; ctx.lineWidth = 1.5; ctx.fillRect(obj.x - 15, obj.y - 15, 30, 30); ctx.strokeRect(obj.x - 15, obj.y - 15, 30, 30); // Draw pole symbol ctx.fillStyle = '#ffffff'; ctx.font = '11px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('⬆', obj.x, obj.y); break; case 'end': // Draw end connection as red square ctx.fillStyle = isConnectionFrom ? '#f59e0b' : '#ef4444'; ctx.strokeStyle = isConnectionFrom ? '#d97706' : '#dc2626'; ctx.lineWidth = 1.5; ctx.fillRect(obj.x - 15, obj.y - 15, 30, 30); ctx.strokeRect(obj.x - 15, obj.y - 15, 30, 30); // Draw end symbol ctx.fillStyle = '#ffffff'; ctx.font = '11px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('⬤', obj.x, obj.y); break; } // Draw labels with better typography ctx.fillStyle = '#374151'; ctx.font = '9px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; if (obj.name) { ctx.fillText(obj.name, obj.x, obj.y + 18); } // Display load information based on type if (obj.type !== 'transformer' && obj.type !== 'pole') { let loadText = ''; if (obj.loadType === 'residential_3phase' && obj.loadUnits) { const correctedLoad = obj.loadUnits * 7 * getCorrectionFactor(obj.loadUnits); loadText = `${obj.loadUnits}×7kW (${correctedLoad.toFixed(1)}kW)`; } else if (obj.loadType === 'residential_1phase' && obj.loadUnits) { const correctedLoad = obj.loadUnits * 4 * getCorrectionFactor(obj.loadUnits); loadText = `${obj.loadUnits}×4kW (${correctedLoad.toFixed(1)}kW)`; } else if (obj.loadType === 'commercial' && obj.commercialLoad) { loadText = `${obj.commercialLoad}kW (kom.)`; } else if (obj.load || obj.powerRating) { loadText = `${obj.load || obj.powerRating}kW`; } if (loadText) { ctx.fillText(loadText, obj.x, obj.y + 30); } } // Draw voltage information if calculation results are available if (calculationResults && calculationResults.nodes) { const nodeResult = calculationResults.nodes.find(n => n.id === obj.id); if (nodeResult) { const voltageText = `${nodeResult.voltage}V`; const dropText = `(-${nodeResult.voltageDrop}V)`; // Voltage label background const voltageColor = nodeResult.isValid ? '#10b981' : '#ef4444'; ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; ctx.strokeStyle = voltageColor; ctx.lineWidth = 1; const bgY = obj.y + (obj.type !== 'transformer' && obj.type !== 'pole' && (obj.loadUnits || obj.commercialLoad || obj.load || obj.powerRating) ? 42 : 30); ctx.fillRect(obj.x - 25, bgY, 50, 12); ctx.strokeRect(obj.x - 25, bgY, 50, 12); // Voltage text ctx.fillStyle = voltageColor; ctx.font = 'bold 8px Arial'; ctx.fillText(voltageText, obj.x, bgY + 2); // Voltage drop text if (nodeResult.voltageDrop > 0.1) { ctx.fillStyle = '#6b7280'; ctx.font = '7px Arial'; ctx.fillText(dropText, obj.x, bgY + 14); } } } ctx.restore(); }); }; const drawCables = () => { cables.forEach(cable => { const fromObj = objects.find(obj => obj.id === cable.from); const toObj = objects.find(obj => obj.id === cable.to); if (!fromObj || !toObj) return; const isSelected = selectedCable && selectedCable.id === cable.id; ctx.save(); // Enable anti-aliasing for smooth lines ctx.imageSmoothingEnabled = true; if (isSelected) { ctx.shadowColor = '#3b82f6'; ctx.shadowBlur = 6; } // Set cable color and style based on type if (cable.cableType === 'YAKY' || cable.cableType === 'NA2XY-J') { ctx.strokeStyle = isSelected ? '#6366f1' : '#8b5cf6'; ctx.setLineDash([]); } else { ctx.strokeStyle = isSelected ? '#f59e0b' : '#d97706'; ctx.setLineDash([6, 3]); } ctx.lineWidth = isSelected ? 3 : 2.5; ctx.lineCap = 'round'; // Rounded line ends for smoother appearance ctx.beginPath(); ctx.moveTo(fromObj.x, fromObj.y); ctx.lineTo(toObj.x, toObj.y); ctx.stroke(); ctx.setLineDash([]); // Draw cable label with background const midX = (fromObj.x + toObj.x) / 2; const midY = (fromObj.y + toObj.y) / 2; let label = cable.label || `${cable.cableType} ${cable.crossSection}mm²`; // Add current information if calculation results are available if (calculationResults && calculationResults.cables) { const cableResult = calculationResults.cables.find(c => c.id === cable.id); if (cableResult && cableResult.current > 0.1) { label += ` (${cableResult.current}A)`; } } // Set font before measuring text ctx.font = '9px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; // Draw background for label ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; ctx.strokeStyle = '#e5e7eb'; ctx.lineWidth = 0.5; const textWidth = ctx.measureText(label).width; const padding = 4; ctx.fillRect(midX - textWidth/2 - padding, midY - 8, textWidth + padding * 2, 16); ctx.strokeRect(midX - textWidth/2 - padding, midY - 8, textWidth + padding * 2, 16); // Draw label text ctx.fillStyle = '#374151'; ctx.fillText(label, midX, midY); ctx.restore(); }); // Draw connection preview when in connect mode if (selectedTool === 'connect' && connectionFrom) { ctx.save(); ctx.strokeStyle = '#f59e0b'; ctx.lineWidth = 2; ctx.setLineDash([4, 4]); ctx.globalAlpha = 0.7; ctx.lineCap = 'round'; // This would need mouse position tracking to work properly // For now, just show the connection source is selected ctx.restore(); } }; drawGrid(); drawObjects(); drawCables(); // Cleanup function return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [objects, cables, selectedObject, selectedCable, connectionFrom, isDragging]); // Canvas event handlers const getCanvasCoordinates = (e) => { const canvas = canvasRef.current; if (!canvas) return { x: 0, y: 0 }; const rect = canvas.getBoundingClientRect(); // For high-DPI displays, we need to use the display size, not the internal canvas size return { x: e.clientX - rect.left, y: e.clientY - rect.top }; }; const snapToGrid = (x, y) => { return { x: Math.round(x / 20) * 20, y: Math.round(y / 20) * 20 }; }; const findObjectAt = (x, y) => { // Search from top to bottom (last drawn objects first) for (let i = objects.length - 1; i >= 0; i--) { const obj = objects[i]; const distance = Math.sqrt((x - obj.x) ** 2 + (y - obj.y) ** 2); if (distance <= 20) { // 20px radius for easier clicking return obj; } } return null; }; const handleCanvasMouseDown = (e) => { e.preventDefault(); const coords = getCanvasCoordinates(e); if (selectedTool === 'select') { // Find clicked object const clickedObject = findObjectAt(coords.x, coords.y); if (clickedObject) { // Select and prepare for dragging setSelectedObject(clickedObject); setSelectedCable(null); setIsDragging(true); setDragOffset({ x: coords.x - clickedObject.x, y: coords.y - clickedObject.y }); } else { // Check for cable selection let clickedCable = null; for (const cable of cables) { const fromObj = objects.find(obj => obj.id === cable.from); const toObj = objects.find(obj => obj.id === cable.to); if (fromObj && toObj) { const dist = distanceToLine(coords.x, coords.y, fromObj.x, fromObj.y, toObj.x, toObj.y); if (dist <= 10) { clickedCable = cable; break; } } } if (clickedCable) { setSelectedCable(clickedCable); setSelectedObject(null); } else { // Clear selection setSelectedObject(null); setSelectedCable(null); } setIsDragging(false); } } else if (selectedTool === 'connect') { const clickedObject = findObjectAt(coords.x, coords.y); if (clickedObject) { if (!connectionFrom) { // First click - select source setConnectionFrom(clickedObject); setError(null); } else { // Second click - create connection if (connectionFrom.id !== clickedObject.id) { // Check if connection already exists const existingCable = cables.find(cable => (cable.from === connectionFrom.id && cable.to === clickedObject.id) || (cable.from === clickedObject.id && cable.to === connectionFrom.id) ); if (!existingCable) { // Calculate distance for cable length const distance = Math.sqrt( (clickedObject.x - connectionFrom.x) ** 2 + (clickedObject.y - connectionFrom.y) ** 2 ); const length = Math.max(Math.round(distance * 0.5), 10); // Convert to meters, min 10m const newCable = { id: Date.now() + Math.random(), // Ensure unique ID from: connectionFrom.id, to: clickedObject.id, label: `Kabel_${cables.length + 1}`, cableType: 'YAKY', crossSection: 25, length: length, type: 'underground', description: '', resistance: 0.727 }; setCables(prev => [...prev, newCable]); setError(null); } else { setError('Połączenie już istnieje między tymi obiektami'); } } else { setError('Nie można połączyć obiektu z samym sobą'); } setConnectionFrom(null); } } else { // Click on empty space - cancel connection setConnectionFrom(null); setError(null); } } else if (['transformer', 'box', 'pole', 'end'].includes(selectedTool)) { // Add new object if (selectedTool === 'transformer') { // Check if transformer already exists const existingTransformer = objects.find(obj => obj.type === 'transformer'); if (existingTransformer) { setError('Tylko jeden transformator dozwolony w projekcie'); return; } } const snapped = snapToGrid(coords.x, coords.y); const newObject = { id: Date.now() + Math.random(), type: selectedTool, x: snapped.x, y: snapped.y, name: `${getObjectTypeName(selectedTool)}_${objects.length + 1}`, number: objects.length + 1, // Type-specific properties upperVoltage: selectedTool === 'transformer' ? 15000 : undefined, bottomVoltage: selectedTool === 'transformer' ? 230 : undefined, power: selectedTool === 'transformer' ? 100 : undefined, powerRating: selectedTool !== 'transformer' && selectedTool !== 'pole' ? 0 : undefined, nodeType: selectedTool !== 'transformer' ? selectedTool : undefined, typeDescription: getDefaultTypeDescription(selectedTool), description: '', voltage: selectedTool === 'transformer' ? undefined : 230, current: 0, // Load properties loadType: selectedTool !== 'transformer' && selectedTool !== 'pole' ? 'residential_3phase' : undefined, loadUnits: selectedTool !== 'transformer' && selectedTool !== 'pole' ? 1 : undefined, commercialLoad: 0, load: 0 }; setObjects(prev => [...prev, newObject]); setSelectedObject(newObject); setSelectedTool('select'); setError(null); } }; const handleCanvasMouseMove = (e) => { if (!isDragging || !selectedObject || selectedTool !== 'select') { return; } e.preventDefault(); const coords = getCanvasCoordinates(e); // Calculate new position const newX = coords.x - dragOffset.x; const newY = coords.y - dragOffset.y; const snapped = snapToGrid(newX, newY); // Update object position setObjects(prev => prev.map(obj => obj.id === selectedObject.id ? { ...obj, x: snapped.x, y: snapped.y } : obj )); // Update selected object state setSelectedObject(prev => ({ ...prev, x: snapped.x, y: snapped.y })); }; const handleCanvasMouseUp = (e) => { e.preventDefault(); setIsDragging(false); }; const handleCanvasMouseLeave = () => { setIsDragging(false); }; const getObjectTypeName = (type) => { switch (type) { case 'transformer': return 'Transformator'; case 'box': return 'Skrzynka'; case 'pole': return 'Słup'; case 'end': return 'Końcówka'; default: return 'Obiekt'; } }; const getDefaultTypeDescription = (type) => { switch (type) { case 'transformer': return 'Transformator 15/0.23kV'; case 'box': return 'Skrzynka kablowa nn'; case 'pole': return 'Słup betonowy'; case 'end': return 'Punkt końcowy'; default: return ''; } }; const distanceToLine = (px, py, x1, y1, x2, y2) => { const dx = x2 - x1; const dy = y2 - y1; const length = Math.sqrt(dx * dx + dy * dy); if (length === 0) return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1)); const t = ((px - x1) * dx + (py - y1) * dy) / (length * length); const projection = { x: x1 + t * dx, y: y1 + t * dy }; if (t < 0) { return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1)); } else if (t > 1) { return Math.sqrt((px - x2) * (px - x2) + (py - y2) * (py - y2)); } else { return Math.sqrt((px - projection.x) * (px - projection.x) + (py - projection.y) * (py - projection.y)); } }; const calculateVoltageDrops = async () => { setIsLoading(true); setError(null); try { // Build network graph and calculate voltage drops locally const results = calculateNetworkVoltageDrops(); setCalculationResults(results); } catch (error) { console.error('Calculation error:', error); setError(error.message || 'Błąd podczas obliczeń'); } finally { setIsLoading(false); } }; const calculateNetworkVoltageDrops = () => { // Find transformer (source node) const transformer = objects.find(obj => obj.type === 'transformer'); if (!transformer) { throw new Error('Brak transformatora w sieci - wymagany jako źródło zasilania'); } // Build network graph const networkGraph = buildNetworkGraph(); // Calculate voltage at each node using depth-first traversal const nodeVoltages = new Map(); const nodeLoads = new Map(); const cableCurrents = new Map(); // Initialize transformer voltage const sourceVoltage = transformer.bottomVoltage || 230; // Default 230V nodeVoltages.set(transformer.id, sourceVoltage); nodeLoads.set(transformer.id, 0); // Calculate loads at each node calculateNodeLoads(networkGraph, nodeLoads); // Calculate voltages using recursive traversal calculateNodeVoltages(transformer.id, networkGraph, nodeVoltages, nodeLoads, cableCurrents, sourceVoltage); // Build results return buildCalculationResults(nodeVoltages, nodeLoads, cableCurrents, sourceVoltage); }; const buildNetworkGraph = () => { const graph = new Map(); // Initialize all nodes objects.forEach(obj => { graph.set(obj.id, { object: obj, connections: [] }); }); // Add cable connections cables.forEach(cable => { const fromNode = graph.get(cable.from); const toNode = graph.get(cable.to); if (fromNode && toNode) { fromNode.connections.push({ nodeId: cable.to, cable: cable, direction: 'outgoing' }); toNode.connections.push({ nodeId: cable.from, cable: cable, direction: 'incoming' }); } }); return graph; }; const getTotalDownstreamConsumers = (graph, nodeId, visited = new Set()) => { if (visited.has(nodeId)) return 0; visited.add(nodeId); const node = graph.get(nodeId); if (!node) return 0; let totalConsumers = 0; // Count consumers at this node (only residential) const obj = node.object; if (obj.type !== 'transformer' && obj.type !== 'pole') { if (obj.loadType === 'residential_3phase') { totalConsumers += obj.loadUnits || 0; } else if (obj.loadType === 'residential_1phase') { // Convert 1-phase to equivalent 3-phase units totalConsumers += Math.ceil((obj.loadUnits || 0) / 3); } // Commercial and manual loads don't count for diversity factor } // Add downstream consumers node.connections.forEach(conn => { if (conn.direction === 'outgoing') { totalConsumers += getTotalDownstreamConsumers(graph, conn.nodeId, visited); } }); return totalConsumers; }; const calculateNodeLoads = (graph, nodeLoads) => { // Calculate total load at each node (including downstream loads) const visited = new Set(); const calculateLoad = (nodeId) => { if (visited.has(nodeId)) return nodeLoads.get(nodeId) || 0; visited.add(nodeId); const node = graph.get(nodeId); if (!node) return 0; // Direct load at this node (without correction factor yet) let directLoad = 0; if (node.object.type !== 'transformer' && node.object.type !== 'pole') { const obj = node.object; // Calculate load based on load type (no correction factor applied here) if (obj.loadType === 'residential_3phase') { // 3-phase residential: 7kW per unit directLoad = (obj.loadUnits || 0) * 7; } else if (obj.loadType === 'residential_1phase') { // 1-phase residential: 4kW per unit directLoad = (obj.loadUnits || 0) * 4; } else if (obj.loadType === 'commercial') { // Commercial load directLoad = obj.commercialLoad || obj.powerRating || 0; } else { // Legacy/manual load entry directLoad = obj.load || obj.powerRating || 0; } } // Calculate downstream loads (without correction factors) let downstreamLoad = 0; node.connections.forEach(conn => { if (conn.direction === 'outgoing') { downstreamLoad += calculateLoad(conn.nodeId); } }); // Total load before applying diversity factor const totalRawLoad = directLoad + downstreamLoad; // Calculate total downstream consumers for diversity factor const totalDownstreamConsumers = getTotalDownstreamConsumers(graph, nodeId, new Set()); const diversityFactor = getCorrectionFactor(totalDownstreamConsumers); // Apply diversity factor to the total downstream load const adjustedLoad = totalRawLoad * diversityFactor; nodeLoads.set(nodeId, adjustedLoad); return adjustedLoad; }; // Calculate loads for all nodes graph.forEach((_, nodeId) => { calculateLoad(nodeId); }); }; const getCorrectionFactor = (units) => { // Współczynnik jednoczesności (diversity factors) for residential loads // Based on Polish electrical engineering standards const diversityFactors = { 1: 1.0, 2: 0.59, 3: 0.45, 4: 0.38, 5: 0.34, 6: 0.31, 7: 0.29, 8: 0.27, 9: 0.26, 10: 0.25, 11: 0.232, 12: 0.217, 13: 0.208, 14: 0.193, 15: 0.183, 16: 0.175, 17: 0.168, 18: 0.161, 19: 0.155, 20: 0.15, 21: 0.145, 22: 0.141, 23: 0.137, 24: 0.133, 25: 0.13, 26: 0.127, 27: 0.124, 28: 0.121, 29: 0.119, 30: 0.117, 31: 0.115, 32: 0.113, 33: 0.111, 34: 0.109, 35: 0.107, 36: 0.105, 37: 0.104, 38: 0.103, 39: 0.101, 40: 0.1, }; // Return the specific factor or default to 0.1 (10%) for >40 consumers return diversityFactors[units] || 0.1; }; const calculateNodeVoltages = (nodeId, graph, nodeVoltages, nodeLoads, cableCurrents, parentVoltage) => { const node = graph.get(nodeId); if (!node) return; // Set voltage for current node if not already set if (!nodeVoltages.has(nodeId)) { nodeVoltages.set(nodeId, parentVoltage); } const currentVoltage = nodeVoltages.get(nodeId); // Process outgoing connections node.connections.forEach(conn => { if (conn.direction === 'outgoing') { const cable = conn.cable; const downstreamNodeId = conn.nodeId; const downstreamLoad = nodeLoads.get(downstreamNodeId) || 0; // Calculate current through this cable const current = downstreamLoad > 0 ? (downstreamLoad * 1000) / (currentVoltage * Math.sqrt(3)) : 0; // I = P/(√3*U) cableCurrents.set(cable.id, current); // Calculate voltage drop in cable const resistance = getCableResistance(cable) || 0.001; // Ω/km const length = (cable.length || 100) / 1000; // Convert m to km const voltageDrop = current * resistance * length * Math.sqrt(3); // 3-phase voltage drop // Calculate voltage at downstream node const downstreamVoltage = currentVoltage - voltageDrop; nodeVoltages.set(downstreamNodeId, downstreamVoltage); // Recursively calculate for downstream nodes calculateNodeVoltages(downstreamNodeId, graph, nodeVoltages, nodeLoads, cableCurrents, downstreamVoltage); } }); }; const buildCalculationResults = (nodeVoltages, nodeLoads, cableCurrents, sourceVoltage) => { const nodeResults = []; const cableResults = []; let maxVoltageDrop = 0; let minVoltage = sourceVoltage; let totalPower = 0; // Build network graph for diversity factor calculations const networkGraph = buildNetworkGraph(); // Process node results nodeVoltages.forEach((voltage, nodeId) => { const obj = objects.find(o => o.id === nodeId); const load = nodeLoads.get(nodeId) || 0; const voltageDrop = sourceVoltage - voltage; const voltageDropPercentage = (voltageDrop / sourceVoltage) * 100; // Calculate total downstream consumers for diversity factor const totalDownstreamConsumers = getTotalDownstreamConsumers(networkGraph, nodeId, new Set()); const diversityFactor = getCorrectionFactor(totalDownstreamConsumers); // Calculate direct load at this node (not including downstream) let directLoad = 0; if (obj && obj.type !== 'transformer' && obj.type !== 'pole') { if (obj.loadType === 'residential_3phase') { directLoad = (obj.loadUnits || 0) * 7; // Raw load without correction } else if (obj.loadType === 'residential_1phase') { directLoad = (obj.loadUnits || 0) * 4; // Raw load without correction } else if (obj.loadType === 'commercial') { directLoad = obj.commercialLoad || 0; } else { directLoad = obj.load || obj.powerRating || 0; } } totalPower += directLoad; // Only count direct loads for total maxVoltageDrop = Math.max(maxVoltageDrop, voltageDrop); minVoltage = Math.min(minVoltage, voltage); nodeResults.push({ id: nodeId, name: obj?.name || `${obj?.type} #${obj?.number}`, type: obj?.type, voltage: Math.round(voltage * 100) / 100, voltageDrop: Math.round(voltageDrop * 100) / 100, voltageDropPercentage: Math.round(voltageDropPercentage * 100) / 100, load: load, directLoad: directLoad, loadType: obj?.loadType, loadUnits: obj?.loadUnits, totalDownstreamConsumers: totalDownstreamConsumers, diversityFactor: diversityFactor, isValid: voltageDropPercentage <= 5, position: { x: obj?.x, y: obj?.y } }); }); // Process cable results cables.forEach(cable => { const current = cableCurrents.get(cable.id) || 0; const fromVoltage = nodeVoltages.get(cable.from) || sourceVoltage; const toVoltage = nodeVoltages.get(cable.to) || sourceVoltage; const cableVoltageDrop = fromVoltage - toVoltage; cableResults.push({ id: cable.id, label: cable.label || `${cable.cableType} ${cable.crossSection}mm²`, from: cable.from, to: cable.to, current: Math.round(current * 100) / 100, voltageDrop: Math.round(cableVoltageDrop * 100) / 100, powerLoss: Math.round(Math.pow(current, 2) * getCableResistance(cable) * (cable.length / 1000) * 3 / 1000 * 100) / 100, utilization: Math.round((current / getCableMaxCurrent(cable)) * 100 * 100) / 100, isOverloaded: current > getCableMaxCurrent(cable) }); }); return { summary: { sourceVoltage: sourceVoltage, totalPower: Math.round(totalPower * 100) / 100, maxVoltageDrop: Math.round(maxVoltageDrop * 100) / 100, maxVoltageDropPercentage: Math.round((maxVoltageDrop / sourceVoltage) * 100 * 100) / 100, minVoltage: Math.round(minVoltage * 100) / 100, networkValid: (maxVoltageDrop / sourceVoltage) * 100 <= 5, nodeCount: objects.length, cableCount: cables.length }, nodes: nodeResults.sort((a, b) => b.voltageDropPercentage - a.voltageDropPercentage), cables: cableResults.sort((a, b) => b.voltageDrop - a.voltageDrop), timestamp: new Date().toISOString() }; }; const exportData = () => { const data = { objects, cables, timestamp: new Date().toISOString() }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `power-system-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); }; const importData = (event) => { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); setObjects(data.objects || []); setCables(data.cables || []); setSelectedObject(null); setError(null); } catch (err) { setError('Błąd podczas wczytywania pliku'); } }; reader.readAsText(file); }; const clearAll = () => { setObjects([]); setCables([]); setSelectedObject(null); setSelectedCable(null); setConnectionFrom(null); setCalculationResults(null); setError(null); }; const updateSelectedObject = (field, value) => { if (!selectedObject) return; const updatedObjects = objects.map(obj => obj.id === selectedObject.id ? { ...obj, [field]: value } : obj ); setObjects(updatedObjects); setSelectedObject({ ...selectedObject, [field]: value }); }; const updateSelectedCable = (field, value) => { if (!selectedCable) return; const updatedCables = cables.map(cable => cable.id === selectedCable.id ? { ...cable, [field]: value } : cable ); setCables(updatedCables); setSelectedCable({ ...selectedCable, [field]: value }); }; const deleteCable = (cableId) => { const updatedCables = cables.filter(cable => cable.id !== cableId); setCables(updatedCables); setSelectedCable(null); }; const deleteObject = (objectId) => { // Remove the object const updatedObjects = objects.filter(obj => obj.id !== objectId); setObjects(updatedObjects); // Remove all cables connected to this object const updatedCables = cables.filter(cable => cable.from !== objectId && cable.to !== objectId ); setCables(updatedCables); setSelectedObject(null); setSelectedCable(null); }; const getCableResistance = (cable) => { const resistanceTable = { 'YAKY': { 16: 1.15, 25: 0.727, 35: 0.524, 50: 0.387, 70: 0.268, 95: 0.193, 120: 0.153, 150: 0.124, 185: 0.099, 240: 0.075 }, 'NA2XY-J': { 16: 1.15, 25: 0.727, 35: 0.524, 50: 0.387, 70: 0.268, 95: 0.193, 120: 0.153, 150: 0.124, 185: 0.099, 240: 0.075 }, 'AL': { 16: 1.91, 25: 1.20, 35: 0.868, 50: 0.641, 70: 0.443, 95: 0.320, 120: 0.253, 150: 0.206, 185: 0.164, 240: 0.125 }, 'AsXSn': { 16: 1.91, 25: 1.20, 35: 0.868, 50: 0.641, 70: 0.443, 95: 0.320, 120: 0.253, 150: 0.206, 185: 0.164, 240: 0.125 } }; const cableType = cable.cableType || 'YAKY'; const crossSection = cable.crossSection || 25; if (resistanceTable[cableType] && resistanceTable[cableType][crossSection]) { return resistanceTable[cableType][crossSection]; } return 1.0; // Default resistance }; const getCableMaxCurrent = (cable) => { const currentTable = { 'YAKY': { 16: 77, 25: 101, 35: 125, 50: 151, 70: 192, 95: 232, 120: 269, 150: 309, 185: 356, 240: 415 }, 'NA2XY-J': { 16: 77, 25: 101, 35: 125, 50: 151, 70: 192, 95: 232, 120: 269, 150: 309, 185: 356, 240: 415 }, 'AL': { 16: 60, 25: 78, 35: 97, 50: 117, 70: 149, 95: 180, 120: 209, 150: 240, 185: 276, 240: 322 }, 'AsXSn': { 16: 60, 25: 78, 35: 97, 50: 117, 70: 149, 95: 180, 120: 209, 150: 240, 185: 276, 240: 322 } }; const cableType = cable.cableType || 'YAKY'; const crossSection = cable.crossSection || 25; if (currentTable[cableType] && currentTable[cableType][crossSection]) { return currentTable[cableType][crossSection]; } return 100; // Default max current in A }; return (
{/* Page Header */}

Projektant Systemów Energetycznych

Narzędzie do projektowania systemów dystrybucji energii elektrycznej

{error && ( {error} )}
{/* Main Content */}
{/* Toolbar */}
{/* Tools Section */}
Tools
{/* Select Tool */} {/* Connect Tool */}
{/* Components Section */}
Components
{/* Transformer */} {/* Cable Box */} {/* Pole */} {/* End Point */}
{/* Actions Section */}
Actions
{/* Clear All */} {/* Export */} {/* Import */}
{/* Project Stats */}
{objects.length}
objects
{/* Canvas Area */}
{/* Connection status indicator */} {selectedTool === 'connect' && connectionFrom && (

Wybrano: {connectionFrom.name}

Kliknij drugi obiekt aby połączyć kablem

)}
{/* Properties Panel */}

Właściwości

{selectedObject ? (

{getObjectTypeName(selectedObject.type)} #{selectedObject.number}

ID: {selectedObject.id}

updateSelectedObject('name', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
updateSelectedObject('number', parseInt(e.target.value))} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
{selectedObject.type !== 'transformer' && (
updateSelectedObject('typeDescription', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="np. Skrzynka typu SRnP" />
)} {selectedObject.type === 'transformer' && ( <>
updateSelectedObject('upperVoltage', parseInt(e.target.value))} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
updateSelectedObject('bottomVoltage', parseInt(e.target.value))} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="230" />
updateSelectedObject('power', parseInt(e.target.value))} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
)} {selectedObject.type !== 'transformer' && selectedObject.type !== 'pole' && ( <>
{selectedObject.loadType === 'residential_3phase' && ( <>
updateSelectedObject('loadUnits', parseInt(e.target.value) || 1)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" min="1" placeholder="1" />
Obliczone obciążenie

{selectedObject.loadUnits || 1} × 7kW = {((selectedObject.loadUnits || 1) * 7).toFixed(2)} kW (raw)

Współczynnik jednoczesności: {(() => { const networkGraph = buildNetworkGraph(); const totalConsumers = getTotalDownstreamConsumers(networkGraph, selectedObject.id, new Set()); return getCorrectionFactor(totalConsumers); })()}
Całkowita liczba odbiorców: {(() => { const networkGraph = buildNetworkGraph(); return getTotalDownstreamConsumers(networkGraph, selectedObject.id, new Set()); })()}
Skorygowana moc: {(() => { const networkGraph = buildNetworkGraph(); const totalConsumers = getTotalDownstreamConsumers(networkGraph, selectedObject.id, new Set()); const factor = getCorrectionFactor(totalConsumers); return ((selectedObject.loadUnits || 1) * 7 * factor).toFixed(2); })()} kW

)} {selectedObject.loadType === 'residential_1phase' && ( <>
updateSelectedObject('loadUnits', parseInt(e.target.value) || 1)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" min="1" placeholder="1" />
Obliczone obciążenie

{selectedObject.loadUnits || 1} × 4kW = {((selectedObject.loadUnits || 1) * 4).toFixed(2)} kW (raw)

Współczynnik jednoczesności: {(() => { const networkGraph = buildNetworkGraph(); const totalConsumers = getTotalDownstreamConsumers(networkGraph, selectedObject.id, new Set()); return getCorrectionFactor(totalConsumers); })()}
Całkowita liczba odbiorców: {(() => { const networkGraph = buildNetworkGraph(); return getTotalDownstreamConsumers(networkGraph, selectedObject.id, new Set()); })()}
Skorygowana moc: {(() => { const networkGraph = buildNetworkGraph(); const totalConsumers = getTotalDownstreamConsumers(networkGraph, selectedObject.id, new Set()); const factor = getCorrectionFactor(totalConsumers); return ((selectedObject.loadUnits || 1) * 4 * factor).toFixed(2); })()} kW

)} {selectedObject.loadType === 'commercial' && ( <>
updateSelectedObject('commercialLoad', parseFloat(e.target.value) || 0)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" min="0" step="0.1" placeholder="0" />
Obciążenie komercyjne

{selectedObject.commercialLoad || 0} kW (bez współczynnika korekcji)

)} {selectedObject.loadType === 'manual' && (
updateSelectedObject('load', parseFloat(e.target.value) || 0)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" min="0" step="0.1" placeholder="0" />
)} )}
X: {selectedObject.x}px
Y: {selectedObject.y}px