1878 lines
96 KiB
JavaScript
1878 lines
96 KiB
JavaScript
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>
|
||
);
|
||
}
|