Files
przekroj/pages/spadki.js

1878 lines
96 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (
<Layout title="Wastpol - Projektant Systemów Energetycznych">
<div className="flex items-center justify-center min-h-screen">
<Card className="w-full max-w-md">
<CardContent className="text-center p-6">
<h2 className="text-xl font-semibold mb-4">Wymagane logowanie</h2>
<Button onClick={() => signIn()}>
Zaloguj się
</Button>
</CardContent>
</Card>
</div>
</Layout>
);
}
// 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 (
<Layout title="Wastpol - Projektant Systemów Energetycznych">
<div className="h-screen flex flex-col">
{/* Page Header */}
<div className="p-6 border-b border-gray-200 bg-white">
<div className="flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2 flex items-center">
<BoltIcon className="w-8 h-8 mr-3 text-yellow-500" />
Projektant Systemów Energetycznych
</h1>
<p className="text-gray-600">
Narzędzie do projektowania systemów dystrybucji energii elektrycznej
</p>
</div>
<div className="flex space-x-2">
<Button
onClick={calculateVoltageDrops}
disabled={isLoading || objects.length === 0}
className="bg-green-600 hover:bg-green-700 text-white"
>
{isLoading ? 'Obliczanie...' : 'Oblicz spadki napięcia'}
</Button>
<Button
onClick={clearAll}
variant="outline"
className="text-red-600 border-red-300 hover:bg-red-50"
>
Wyczyść wszystko
</Button>
<Button onClick={exportData} variant="outline">
Eksportuj dane
</Button>
<label>
<Button as="span" variant="outline">
Importuj dane
</Button>
<input
type="file"
accept=".json"
onChange={importData}
className="hidden"
/>
</label>
</div>
</div>
{error && (
<Alert variant="destructive" className="mt-4">
{error}
</Alert>
)}
</div>
{/* Main Content */}
<div className="flex flex-1 overflow-hidden">
{/* Toolbar */}
<div className="w-16 bg-white border-r border-gray-200 flex flex-col">
{/* Tools Section */}
<div className="flex flex-col p-2 border-b border-gray-200">
<div className="text-xs text-gray-500 mb-2 text-center">Tools</div>
{/* Select Tool */}
<button
onClick={() => setSelectedTool('select')}
className={`
w-12 h-12 rounded-lg mb-1 flex items-center justify-center transition-all duration-200
${selectedTool === 'select'
? 'bg-blue-600 text-white shadow-lg'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
}
`}
title="Select Tool (V)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122" />
</svg>
</button>
{/* Connect Tool */}
<button
onClick={() => setSelectedTool('connect')}
className={`
w-12 h-12 rounded-lg flex items-center justify-center transition-all duration-200
${selectedTool === 'connect'
? 'bg-blue-600 text-white shadow-lg'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
}
`}
title="Connect Tool (C)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</button>
</div>
{/* Components Section */}
<div className="flex flex-col p-2 border-b border-gray-200">
<div className="text-xs text-gray-500 mb-2 text-center">Components</div>
{/* Transformer */}
<button
onClick={() => setSelectedTool('transformer')}
className={`
w-12 h-12 rounded-lg mb-1 flex items-center justify-center transition-all duration-200 relative
${selectedTool === 'transformer'
? 'bg-green-600 text-white shadow-lg'
: 'text-gray-600 hover:bg-gray-100 hover:text-green-600'
}
`}
title="Transformer (T)"
>
<div
className="w-6 h-6"
style={{
clipPath: 'polygon(50% 0%, 0% 100%, 100% 100%)',
backgroundColor: 'currentColor'
}}
></div>
</button>
{/* Cable Box */}
<button
onClick={() => setSelectedTool('box')}
className={`
w-12 h-12 rounded-lg mb-1 flex items-center justify-center transition-all duration-200
${selectedTool === 'box'
? 'bg-blue-600 text-white shadow-lg'
: 'text-gray-600 hover:bg-gray-100 hover:text-blue-600'
}
`}
title="Cable Box (B)"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<rect x="4" y="4" width="16" height="16" rx="2" />
<rect x="8" y="8" width="8" height="2" fill="white" />
<rect x="8" y="12" width="8" height="2" fill="white" />
</svg>
</button>
{/* Pole */}
<button
onClick={() => setSelectedTool('pole')}
className={`
w-12 h-12 rounded-lg mb-1 flex items-center justify-center transition-all duration-200
${selectedTool === 'pole'
? 'bg-gray-600 text-white shadow-lg'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-700'
}
`}
title="Pole (P)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19V5m0 0l-7 7m7-7l7 7" />
<rect x="6" y="18" width="12" height="2" fill="currentColor" />
</svg>
</button>
{/* End Point */}
<button
onClick={() => setSelectedTool('end')}
className={`
w-12 h-12 rounded-lg flex items-center justify-center transition-all duration-200
${selectedTool === 'end'
? 'bg-red-600 text-white shadow-lg'
: 'text-gray-600 hover:bg-gray-100 hover:text-red-600'
}
`}
title="End Point (E)"
>
<div className="w-6 h-6 rounded-full bg-current"></div>
</button>
</div>
{/* Actions Section */}
<div className="flex flex-col p-2 mt-auto">
<div className="text-xs text-gray-500 mb-2 text-center">Actions</div>
{/* Clear All */}
<button
onClick={clearAll}
className="w-12 h-12 rounded-lg mb-1 flex items-center justify-center text-gray-600 hover:bg-red-50 hover:text-red-600 transition-all duration-200"
title="Clear All (⌘⌫)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
{/* Export */}
<button
onClick={exportData}
className="w-12 h-12 rounded-lg mb-1 flex items-center justify-center text-gray-600 hover:bg-gray-100 hover:text-blue-600 transition-all duration-200"
title="Export Project (⌘S)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</button>
{/* Import */}
<label className="w-12 h-12 rounded-lg flex items-center justify-center text-gray-600 hover:bg-gray-100 hover:text-green-600 transition-all duration-200 cursor-pointer"
title="Import Project (⌘O)">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
</svg>
<input
type="file"
accept=".json"
onChange={importData}
className="hidden"
/>
</label>
</div>
{/* Project Stats */}
<div className="p-2 border-t border-gray-200">
<div className="text-xs text-gray-500 text-center">
<div>{objects.length}</div>
<div className="text-gray-400">objects</div>
</div>
</div>
</div>
{/* Canvas Area */}
<div className="flex-1 relative">
<canvas
ref={canvasRef}
className={`w-full h-full ${
selectedTool === 'select' ? 'cursor-pointer' :
selectedTool === 'connect' ? 'cursor-crosshair' :
'cursor-crosshair'
}`}
style={{ imageRendering: 'crisp-edges' }}
onMouseDown={handleCanvasMouseDown}
onMouseMove={handleCanvasMouseMove}
onMouseUp={handleCanvasMouseUp}
onMouseLeave={handleCanvasMouseLeave}
/>
{/* Connection status indicator */}
{selectedTool === 'connect' && connectionFrom && (
<div className="absolute top-4 left-4 bg-yellow-100 border border-yellow-300 rounded-lg p-3">
<p className="text-sm text-yellow-800">
Wybrano: <span className="font-semibold">{connectionFrom.name}</span>
</p>
<p className="text-xs text-yellow-600">
Kliknij drugi obiekt aby połączyć kablem
</p>
</div>
)}
</div>
{/* Properties Panel */}
<div className="w-80 bg-white border-l border-gray-200 p-4 overflow-y-auto">
<h3 className="text-lg font-medium text-gray-900 mb-4">Właściwości</h3>
{selectedObject ? (
<div className="space-y-4">
<div className="border-b border-gray-200 pb-2 mb-4">
<h4 className="text-sm font-semibold text-gray-900">
{getObjectTypeName(selectedObject.type)} #{selectedObject.number}
</h4>
<p className="text-xs text-gray-500">ID: {selectedObject.id}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nazwa
</label>
<input
type="text"
value={selectedObject.name || ''}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Numer
</label>
<input
type="number"
value={selectedObject.number || ''}
onChange={(e) => 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"
/>
</div>
{selectedObject.type !== 'transformer' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Opis typu
</label>
<input
type="text"
value={selectedObject.typeDescription || ''}
onChange={(e) => 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"
/>
</div>
)}
{selectedObject.type === 'transformer' && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Napięcie pierwotne (V)
</label>
<input
type="number"
value={selectedObject.upperVoltage || ''}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Napięcie wtórne (V)
</label>
<input
type="number"
value={selectedObject.bottomVoltage || ''}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Moc znamionowa (kVA)
</label>
<input
type="number"
value={selectedObject.power || ''}
onChange={(e) => 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"
/>
</div>
</>
)}
{selectedObject.type !== 'transformer' && selectedObject.type !== 'pole' && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Typ obciążenia
</label>
<select
value={selectedObject.loadType || 'residential_3phase'}
onChange={(e) => updateSelectedObject('loadType', 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"
>
<option value="residential_3phase">Mieszkalne 3-fazowe (7kW)</option>
<option value="residential_1phase">Mieszkalne 1-fazowe (4kW)</option>
<option value="commercial">Komercyjne (bez korekcji)</option>
<option value="manual">Ręczne wprowadzenie</option>
</select>
</div>
{selectedObject.loadType === 'residential_3phase' && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Liczba jednostek mieszkalnych (×7kW)
</label>
<input
type="number"
value={selectedObject.loadUnits || ''}
onChange={(e) => 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"
/>
</div>
<div className="bg-blue-50 p-3 rounded-lg">
<h6 className="text-xs font-medium text-blue-800 mb-1">Obliczone obciążenie</h6>
<p className="text-sm text-blue-700">
{selectedObject.loadUnits || 1} × 7kW = {((selectedObject.loadUnits || 1) * 7).toFixed(2)} kW (raw)
</p>
<p className="text-xs text-blue-600">
Współczynnik jednoczesności: {(() => {
const networkGraph = buildNetworkGraph();
const totalConsumers = getTotalDownstreamConsumers(networkGraph, selectedObject.id, new Set());
return getCorrectionFactor(totalConsumers);
})()}<br />
Całkowita liczba odbiorców: {(() => {
const networkGraph = buildNetworkGraph();
return getTotalDownstreamConsumers(networkGraph, selectedObject.id, new Set());
})()}<br />
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
</p>
</div>
</>
)}
{selectedObject.loadType === 'residential_1phase' && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Liczba jednostek mieszkalnych (×4kW)
</label>
<input
type="number"
value={selectedObject.loadUnits || ''}
onChange={(e) => 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"
/>
</div>
<div className="bg-green-50 p-3 rounded-lg">
<h6 className="text-xs font-medium text-green-800 mb-1">Obliczone obciążenie</h6>
<p className="text-sm text-green-700">
{selectedObject.loadUnits || 1} × 4kW = {((selectedObject.loadUnits || 1) * 4).toFixed(2)} kW (raw)
</p>
<p className="text-xs text-green-600">
Współczynnik jednoczesności: {(() => {
const networkGraph = buildNetworkGraph();
const totalConsumers = getTotalDownstreamConsumers(networkGraph, selectedObject.id, new Set());
return getCorrectionFactor(totalConsumers);
})()}<br />
Całkowita liczba odbiorców: {(() => {
const networkGraph = buildNetworkGraph();
return getTotalDownstreamConsumers(networkGraph, selectedObject.id, new Set());
})()}<br />
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
</p>
</div>
</>
)}
{selectedObject.loadType === 'commercial' && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Obciążenie komercyjne (kW)
</label>
<input
type="number"
value={selectedObject.commercialLoad || ''}
onChange={(e) => 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"
/>
</div>
<div className="bg-purple-50 p-3 rounded-lg">
<h6 className="text-xs font-medium text-purple-800 mb-1">Obciążenie komercyjne</h6>
<p className="text-sm text-purple-700">
{selectedObject.commercialLoad || 0} kW (bez współczynnika korekcji)
</p>
</div>
</>
)}
{selectedObject.loadType === 'manual' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Obciążenie ręczne (kW)
</label>
<input
type="number"
value={selectedObject.load || ''}
onChange={(e) => 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"
/>
</div>
)}
</>
)}
<div className="grid grid-cols-2 gap-2 text-xs text-gray-600">
<div>X: {selectedObject.x}px</div>
<div>Y: {selectedObject.y}px</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Opis
</label>
<textarea
value={selectedObject.description || ''}
onChange={(e) => updateSelectedObject('description', e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Dodatkowe informacje..."
/>
</div>
<Button
onClick={() => deleteObject(selectedObject.id)}
variant="outline"
className="w-full text-red-600 border-red-300 hover:bg-red-50"
>
Usuń obiekt
</Button>
</div>
) : selectedCable ? (
<div className="space-y-4">
<div className="border-b border-gray-200 pb-2 mb-4">
<h4 className="text-sm font-semibold text-gray-900">
Kabel połączeniowy
</h4>
<p className="text-xs text-gray-500">ID: {selectedCable.id}</p>
<p className="text-xs text-gray-500">
{objects.find(obj => obj.id === selectedCable.from)?.name} {objects.find(obj => obj.id === selectedCable.to)?.name}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Etykieta
</label>
<input
type="text"
value={selectedCable.label || ''}
onChange={(e) => updateSelectedCable('label', 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. Kabel_1"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Typ kabla
</label>
<select
value={selectedCable.cableType || 'YAKY'}
onChange={(e) => updateSelectedCable('cableType', 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"
>
<option value="YAKY">YAKY (podziemny)</option>
<option value="NA2XY-J">NA2XY-J (podziemny)</option>
<option value="AL">AL (napowietrzny)</option>
<option value="AsXSn">AsXSn (napowietrzny)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Przekrój (mm²)
</label>
<select
value={selectedCable.crossSection || 25}
onChange={(e) => updateSelectedCable('crossSection', 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"
>
<option value={16}>16</option>
<option value={25}>25</option>
<option value={35}>35</option>
<option value={50}>50</option>
<option value={70}>70</option>
<option value={95}>95</option>
<option value={120}>120</option>
<option value={150}>150</option>
<option value={185}>185</option>
<option value={240}>240</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Długość (m)
</label>
<input
type="number"
value={selectedCable.length || ''}
onChange={(e) => updateSelectedCable('length', 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"
min="1"
/>
</div>
<div className="bg-gray-50 p-3 rounded-lg">
<h5 className="text-xs font-medium text-gray-700 mb-1">Parametry elektryczne</h5>
<p className="text-xs text-gray-600">
Rezystancja: {getCableResistance(selectedCable)?.toFixed(4) || 'N/A'} Ω/km
</p>
<p className="text-xs text-gray-600">
Typ instalacji: {(selectedCable.cableType === 'YAKY' || selectedCable.cableType === 'NA2XY-J') ? 'Podziemna' : 'Napowietrzna'}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Opis
</label>
<textarea
value={selectedCable.description || ''}
onChange={(e) => updateSelectedCable('description', e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Dodatkowe informacje o kablu..."
/>
</div>
<Button
onClick={() => deleteCable(selectedCable.id)}
variant="outline"
className="w-full text-red-600 border-red-300 hover:bg-red-50"
>
Usuń kabel
</Button>
</div>
) : calculationResults ? (
<div className="space-y-4">
<h4 className="text-lg font-medium text-gray-900">Analiza spadków napięcia</h4>
{/* Network Summary */}
<div className={`p-4 rounded-lg border ${calculationResults.summary.networkValid ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50'}`}>
<h5 className="font-medium text-gray-700 mb-3">Podsumowanie sieci</h5>
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-gray-600">Napięcie źródłowe:</span>
<div className="font-medium">{calculationResults.summary.sourceVoltage}V</div>
</div>
<div>
<span className="text-gray-600">Moc całkowita:</span>
<div className="font-medium">{calculationResults.summary.totalPower} kW</div>
</div>
<div>
<span className="text-gray-600">Maks. spadek:</span>
<div className={`font-medium ${calculationResults.summary.maxVoltageDropPercentage > 5 ? 'text-red-600' : 'text-green-600'}`}>
{calculationResults.summary.maxVoltageDrop}V ({calculationResults.summary.maxVoltageDropPercentage}%)
</div>
</div>
<div>
<span className="text-gray-600">Min. napięcie:</span>
<div className="font-medium">{calculationResults.summary.minVoltage}V</div>
</div>
</div>
<div className="mt-3">
<span className={`text-xs px-2 py-1 rounded ${calculationResults.summary.networkValid ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{calculationResults.summary.networkValid ? 'Sieć poprawna' : 'Przekroczone normy spadków napięcia'}
</span>
</div>
</div>
{/* Node Analysis */}
<div className="space-y-3">
<h5 className="font-medium text-gray-700">Analiza węzłów ({calculationResults.nodes.length})</h5>
<div className="max-h-64 overflow-y-auto space-y-2">
{calculationResults.nodes.map((node) => (
<div key={node.id} className={`p-3 rounded-lg border text-sm ${node.isValid ? 'border-gray-200 bg-gray-50' : 'border-red-200 bg-red-50'}`}>
<div className="flex justify-between items-start mb-2">
<div>
<span className="font-medium">{node.name}</span>
<span className="text-xs text-gray-500 ml-2">({node.type})</span>
</div>
<span className={`text-xs px-2 py-1 rounded ${node.isValid ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{node.voltageDropPercentage}%
</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs text-gray-600">
<div>Napięcie: <span className="font-medium text-gray-900">{node.voltage}V</span></div>
<div>Spadek: <span className="font-medium text-gray-900">{node.voltageDrop}V</span></div>
{node.directLoad > 0 && (
<>
<div>Obciążenie bezp.: <span className="font-medium text-gray-900">{node.directLoad.toFixed(1)}kW</span></div>
<div>Obciążenie całk.: <span className="font-medium text-gray-900">{node.load.toFixed(1)}kW</span></div>
</>
)}
{node.loadType === 'residential_3phase' && (
<div className="col-span-2 text-blue-600">
{node.loadUnits}×7kW mieszk. 3-faz. (k={node.diversityFactor || getCorrectionFactor(node.loadUnits || 1)})
{node.totalDownstreamConsumers && node.totalDownstreamConsumers !== (node.loadUnits || 1) && (
<span className="text-xs text-gray-500"> - {node.totalDownstreamConsumers} odbiorców</span>
)}
</div>
)}
{node.loadType === 'residential_1phase' && (
<div className="col-span-2 text-green-600">
{node.loadUnits}×4kW mieszk. 1-faz. (k={node.diversityFactor || getCorrectionFactor(Math.ceil((node.loadUnits || 1) / 3))})
{node.totalDownstreamConsumers && node.totalDownstreamConsumers !== Math.ceil((node.loadUnits || 1) / 3) && (
<span className="text-xs text-gray-500"> - {node.totalDownstreamConsumers} odbiorców</span>
)}
</div>
)}
{node.loadType === 'commercial' && (
<div className="col-span-2 text-purple-600">
Komercyjne (bez korekcji)
</div>
)}
</div>
</div>
))}
</div>
</div>
{/* Cable Analysis */}
<div className="space-y-3">
<h5 className="font-medium text-gray-700">Analiza kabli ({calculationResults.cables.length})</h5>
<div className="max-h-64 overflow-y-auto space-y-2">
{calculationResults.cables.map((cable) => (
<div key={cable.id} className={`p-3 rounded-lg border text-sm ${cable.isOverloaded ? 'border-red-200 bg-red-50' : 'border-gray-200 bg-gray-50'}`}>
<div className="flex justify-between items-start mb-2">
<span className="font-medium">{cable.label}</span>
<div className="text-right">
<div className={`text-xs px-2 py-1 rounded ${cable.isOverloaded ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'}`}>
{cable.utilization}% obciążenia
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-xs text-gray-600">
<div>Prąd: <span className="font-medium text-gray-900">{cable.current}A</span></div>
<div>Spadek U: <span className="font-medium text-gray-900">{cable.voltageDrop}V</span></div>
<div>Straty mocy: <span className="font-medium text-gray-900">{cable.powerLoss}kW</span></div>
</div>
</div>
))}
</div>
</div>
{/* Export Results */}
<div className="pt-4 border-t border-gray-200">
<button
onClick={() => {
const data = {
...calculationResults,
project: { objects, cables }
};
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 = `voltage-analysis-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
}}
className="w-full px-3 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Eksportuj wyniki analizy
</button>
</div>
</div>
) : (
<p className="text-gray-500">Brak wybranego obiektu</p>
)}
</div>
</div>
</div>
</Layout>
);
}