diff --git a/.gitignore b/.gitignore index dbc0b53..e5c7b2a 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,6 @@ a.py /pages/api/auth/* /cypress -cypress.json \ No newline at end of file +cypress.json + +/tool-templates \ No newline at end of file diff --git a/ADDING_NEW_TOOLS_GUIDE.md b/ADDING_NEW_TOOLS_GUIDE.md new file mode 100644 index 0000000..278c584 --- /dev/null +++ b/ADDING_NEW_TOOLS_GUIDE.md @@ -0,0 +1,465 @@ +# Adding New Tools and Functionality Guide + +This document provides a comprehensive guide for adding new tools and functionality to the Wastpol web application. It's designed to help developers (including AI agents) integrate new tools from separate projects into the main application. + +## Project Overview + +The Wastpol application is a Next.js-based web application with the following key characteristics: +- **Framework**: Next.js 12 with React 17 +- **Styling**: Tailwind CSS with custom UI components +- **Authentication**: NextAuth.js +- **Architecture**: Pages-based routing with component-driven design +- **UI Library**: Custom components + Evergreen UI + Heroicons + +## Project Structure + +``` +web-app/ +├── pages/ # Next.js pages (routes) +│ ├── api/ # API endpoints +│ ├── index.js # Main dashboard with tabs +│ ├── cross.js # Grid tool +│ ├── uziomy.js # Ground connections tool +│ ├── tracking.js # Package tracking +│ └── [other-tools].js # Additional tools +├── components/ +│ ├── ui/ # Reusable UI components +│ │ ├── Layout.js # Main layout with navigation +│ │ └── components.js # UI component library +│ └── templates/ # Tool-specific components +├── styles/ # Global styles +├── public/ # Static assets +└── externals/ # External data files +``` + +## Adding a New Tool - Step by Step + +### 1. Planning Phase + +Before adding a new tool, define: +- **Tool Purpose**: What does the tool do? +- **Tool Name**: Short, descriptive name +- **Route**: URL path (e.g., `/new-tool`) +- **Icon**: Choose from Heroicons +- **Dependencies**: Any new packages needed +- **API Requirements**: Backend processing needs + +### 2. Tool Integration Checklist + +#### A. Create the Main Page Component + +Create a new file: `pages/[tool-name].js` + +**Template Structure:** +```javascript +import { useState, useCallback } 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 { [ToolIcon] } from '@heroicons/react/24/outline'; + +export default function NewTool() { + const { data: session } = useSession(); + const [isLoading, setIsLoading] = useState(false); + + // Tool-specific state variables + const [toolData, setToolData] = useState(null); + + // Authentication check + if (!session) { + return ( + +
+ + +

Wymagane logowanie

+ +
+
+
+
+ ); + } + + return ( + +
+ {/* Page Header */} +
+

+ New Tool Name +

+

+ Tool description here +

+
+ + {/* Main Content */} + + + + <[ToolIcon] className="w-6 h-6 mr-2" /> + Tool Interface + + + Brief description of what this tool does + + + + {/* Tool-specific UI goes here */} + + +
+
+ ); +} +``` + +#### B. Add Navigation Entry + +Update `components/ui/Layout.js`: + +1. **Import the icon:** +```javascript +import { + // ...existing imports + [NewToolIcon] +} from '@heroicons/react/24/outline'; +``` + +2. **Add to navigationItems array:** +```javascript +const navigationItems = [ + // ...existing items + { name: 'New Tool Name', href: '/new-tool', icon: NewToolIcon }, +]; +``` + +#### C. Create API Endpoint (if needed) + +Create `pages/api/[tool-name].js`: + +```javascript +import formidable from 'formidable'; +import { getSession } from 'next-auth/react'; + +export const config = { + api: { + bodyParser: false, + }, +}; + +export default async function handler(req, res) { + // Authentication check + const session = await getSession({ req }); + if (!session) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + // Tool-specific processing logic + const form = new formidable.IncomingForm(); + const [fields, files] = await form.parse(req); + + // Process the request + const result = await processToolData(fields, files); + + res.status(200).json({ + success: true, + data: result + }); + } catch (error) { + console.error('Tool processing error:', error); + res.status(500).json({ + error: 'Internal server error', + details: error.message + }); + } +} + +async function processToolData(fields, files) { + // Implement tool-specific logic here + return {}; +} +``` + +#### D. Add Tool-Specific Components (Optional) + +Create reusable components in `components/templates/[tool-name]/`: + +```javascript +// components/templates/new-tool/ToolComponent.js +import { useState } from 'react'; +import { Button, Alert } from '../../ui/components'; + +export default function ToolComponent({ onSubmit, isLoading }) { + const [inputData, setInputData] = useState(''); + + const handleSubmit = (e) => { + e.preventDefault(); + onSubmit(inputData); + }; + + return ( +
+ {/* Tool-specific form elements */} + +
+ ); +} +``` + +### 3. Integration from Separate Project + +#### A. Identify Components to Port + +When integrating from a separate project: + +1. **Identify core functionality files** +2. **Extract reusable components** +3. **Map dependencies and APIs** +4. **Identify data processing logic** + +#### B. Dependency Management + +1. **Check existing dependencies** in `package.json` +2. **Add new dependencies** if needed: +```bash +npm install [new-package] +``` +3. **Update package.json** if manual editing is needed + +#### C. Asset Management + +1. **Static files**: Place in `public/` directory +2. **External data**: Place in `externals/` directory +3. **Uploads**: Use `public/uploads/` for user uploads + +### 4. Common Integration Patterns + +#### File Upload Pattern +```javascript +import { FileUploader, FileCard } from "evergreen-ui"; + +const [files, setFiles] = useState([]); +const handleChange = useCallback((files) => setFiles([files[0]]), []); +``` + +#### API Call Pattern +```javascript +import axios from "axios"; + +const handleSubmit = async (data) => { + setIsLoading(true); + try { + const formData = new FormData(); + formData.append("data", JSON.stringify(data)); + + const response = await axios.post("/api/new-tool", formData); + setResult(response.data); + } catch (error) { + setError(error.message); + } finally { + setIsLoading(false); + } +}; +``` + +#### State Management Pattern +```javascript +const [isLoading, setIsLoading] = useState(false); +const [result, setResult] = useState(null); +const [error, setError] = useState(null); +``` + +### 5. Styling Guidelines + +#### Use Existing UI Components +```javascript +import { + Card, + CardHeader, + CardContent, + CardTitle, + CardDescription, + Button, + Alert +} from "../components/ui/components"; +``` + +#### Consistent Layout Classes +- **Container**: `p-6 max-w-7xl mx-auto` +- **Page Header**: `mb-8` +- **Title**: `text-3xl font-bold text-gray-900 mb-2` +- **Description**: `text-gray-600` +- **Cards**: Use existing Card components +- **Spacing**: `space-y-4` for form elements + +### 6. Error Handling Best Practices + +#### Client-Side Error Handling +```javascript +const [error, setError] = useState(null); + +// Clear error on new action +const clearError = () => setError(null); + +// Display errors +{error && ( + + {error} + +)} +``` + +#### Server-Side Error Handling +```javascript +try { + // Process request +} catch (error) { + console.error('Tool error:', error); + return res.status(500).json({ + error: 'Processing failed', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }); +} +``` + +### 7. Testing Integration + +#### Manual Testing Checklist +- [ ] Tool loads without errors +- [ ] Authentication works correctly +- [ ] Navigation item appears and works +- [ ] File uploads work (if applicable) +- [ ] API endpoints respond correctly +- [ ] Results display properly +- [ ] Error handling works +- [ ] Mobile responsiveness +- [ ] Loading states display correctly + +#### Console Testing +1. Check browser console for errors +2. Verify API responses in Network tab +3. Test with different user roles + +### 8. AI Agent Instructions + +When an AI agent is adding a new tool: + +#### Information to Gather +1. **Tool description and purpose** +2. **Required input/output formats** +3. **Processing logic requirements** +4. **UI/UX preferences** +5. **Integration points with existing system** + +#### Step-by-Step Process +1. **Analyze the existing project structure** +2. **Identify the closest existing tool for reference** +3. **Create the main page component** +4. **Add navigation entry** +5. **Create API endpoint if needed** +6. **Test the integration** +7. **Handle any errors or conflicts** + +#### Code Quality Standards +- Follow existing naming conventions +- Use consistent styling patterns +- Implement proper error handling +- Add loading states for async operations +- Ensure responsive design +- Maintain authentication requirements + +### 9. Example Tool Addition + +Here's how to add a hypothetical "Document Generator" tool: + +#### Step 1: Create `pages/document-generator.js` +```javascript +import { useState } from "react"; +import { useSession, signIn } from "next-auth/react"; +import Layout from "../components/ui/Layout"; +import { Card, CardHeader, CardContent, CardTitle } from "../components/ui/components"; +import { DocumentTextIcon } from '@heroicons/react/24/outline'; + +export default function DocumentGenerator() { + const { data: session } = useSession(); + // Component implementation... +} +``` + +#### Step 2: Update `components/ui/Layout.js` +```javascript +import { DocumentTextIcon } from '@heroicons/react/24/outline'; + +const navigationItems = [ + // ...existing items + { name: 'Generator dokumentów', href: '/document-generator', icon: DocumentTextIcon }, +]; +``` + +#### Step 3: Create `pages/api/document-generator.js` +```javascript +export default async function handler(req, res) { + // API implementation... +} +``` + +### 10. Maintenance and Updates + +#### Version Control +- Commit changes with descriptive messages +- Tag releases for major tool additions +- Document breaking changes + +#### Documentation Updates +- Update this guide when patterns change +- Document new dependencies +- Keep API documentation current + +#### Performance Considerations +- Monitor bundle size with new dependencies +- Optimize images and assets +- Consider lazy loading for heavy components + +--- + +## Quick Reference Commands + +```bash +# Install new dependency +npm install [package-name] + +# Run development server +npm run dev + +# Build for production +npm run build + +# Deploy changes +npm run deploy +``` + +## Common File Locations + +- **New page**: `pages/[tool-name].js` +- **API endpoint**: `pages/api/[tool-name].js` +- **Components**: `components/templates/[tool-name]/` +- **Navigation**: `components/ui/Layout.js` +- **Styles**: `styles/globals.css` +- **Assets**: `public/` + +This guide should provide everything needed to successfully integrate new tools into the Wastpol application while maintaining consistency and code quality. diff --git a/SPADKI_INTEGRATION_SUMMARY.md b/SPADKI_INTEGRATION_SUMMARY.md new file mode 100644 index 0000000..4bb0369 --- /dev/null +++ b/SPADKI_INTEGRATION_SUMMARY.md @@ -0,0 +1,153 @@ +# Power System Designer - Integration Summary + +## ✅ **Fixes and Improvements Implemented** + +### **1. Fixed Object Selection** +- **Issue**: Selection tool didn't work properly +- **Fix**: + - Implemented proper `findObjectAt()` function with correct hit detection + - Added visual feedback for selected objects (blue glow) + - Improved click detection accuracy with 20px radius + +### **2. Added Object Dragging** +- **Issue**: Objects couldn't be moved +- **Fix**: + - Implemented `handleCanvasMouseDown`, `handleCanvasMouseMove`, and `handleCanvasMouseUp` event handlers + - Added drag state management with `isDragging` and `dragOffset` + - Objects snap to 20px grid while dragging + - Visual feedback during dragging operations + +### **3. Fixed Cable Connection Tool** +- **Issue**: Cable connection tool didn't work at all +- **Fix**: + - Proper two-click connection workflow + - Visual feedback showing selected connection source (orange highlight) + - Connection status indicator overlay + - Automatic cable length calculation based on object positions + - Prevention of duplicate connections and self-connections + - Error messages for invalid connections + +### **4. Enhanced Object Data Structure** +- **Issue**: Objects didn't have all necessary information +- **Fix**: + - Added comprehensive object properties: + - `typeDescription` - Custom type descriptions + - `load` - Current load vs rated power + - `voltage` - Operating voltage + - `current` - Current flow (for calculations) + - Position tracking (`x`, `y`) + - Proper default values for each object type + +### **5. Improved Cable Data Structure** +- **Enhanced Cable Properties**: + - `resistance` - Electrical resistance based on cable type/size + - Better cable type handling (underground vs overhead) + - Automatic resistance calculation + - Improved cable length calculation + +### **6. Enhanced User Interface** + +#### **Object Properties Panel**: +- Object type indicator with ID +- Position display (X, Y coordinates) +- Type-specific property fields +- Enhanced transformer properties (primary/secondary voltage, power rating) +- Load vs power rating for distribution points +- Custom type descriptions + +#### **Cable Properties Panel**: +- Connection source/destination display +- Electrical parameters (resistance, installation type) +- Cable type selection with proper underground/overhead distinction +- Cross-section selection with standard sizes +- Delete cable functionality + +#### **Visual Improvements**: +- Better object highlighting (selected vs connection source) +- Improved cable rendering with proper colors: + - Purple: Underground cables (YAKY, NA2XY-J) + - Orange: Overhead cables (AL, AsXSn) + - Dashed lines for overhead cables +- Cable labels with background for better readability +- Connection status indicator overlay + +### **7. Added Interaction Features** + +#### **Mouse Controls**: +- **Left Click**: Select/place objects +- **Drag**: Move selected objects (with grid snapping) +- **Connect Mode**: Two-click cable connection workflow + +#### **Keyboard Shortcuts**: +- **Delete/Backspace**: Delete selected object or cable +- **Escape**: Clear selection and return to select mode + +#### **Object Management**: +- Delete objects (with automatic cable cleanup) +- Delete cables individually +- Proper state management for selections + +### **8. Error Handling and Validation** +- Transformer limit enforcement (only one allowed) +- Duplicate connection prevention +- Self-connection prevention +- Error messages with clear descriptions +- Input validation for numeric fields + +### **9. Technical Improvements** + +#### **Event System**: +- Proper mouse event handling with coordinate transformation +- Grid snapping for precise placement +- Drag offset calculation for smooth object movement + +#### **State Management**: +- Clean separation of object and cable selections +- Proper state updates with React patterns +- Automatic re-rendering on state changes + +#### **Performance**: +- Efficient hit detection algorithms +- Optimized canvas rendering +- Proper event listener cleanup + +## **🎯 Key Features Now Working** + +1. **✅ Object Selection**: Click objects to select and edit properties +2. **✅ Object Movement**: Drag objects to new positions with grid snapping +3. **✅ Cable Connections**: Two-click workflow to connect objects +4. **✅ Property Editing**: Comprehensive property panels for all object types +5. **✅ Visual Feedback**: Clear indication of selected items and connection states +6. **✅ Delete Functions**: Remove objects and cables with keyboard shortcuts +7. **✅ Data Persistence**: Export/import with complete object data +8. **✅ Electrical Calculations**: Ready for voltage drop analysis + +## **🔧 Usage Instructions** + +### **Adding Objects**: +1. Click object type button in toolbar (Transformer, Box, Pole, End Connection) +2. Click on canvas to place object +3. Object automatically snaps to grid + +### **Selecting and Moving**: +1. Click "Wybierz" (Select) tool +2. Click object to select (blue highlight appears) +3. Drag selected object to move (snaps to grid) + +### **Connecting with Cables**: +1. Click "Połącz kablem" (Connect with Cable) tool +2. Click first object (orange highlight appears) +3. Click second object to create cable connection +4. Cable appears with automatic length calculation + +### **Editing Properties**: +1. Select object or cable +2. Use properties panel on right to edit all parameters +3. Changes are saved automatically + +### **Deleting Items**: +1. Select object or cable +2. Press Delete/Backspace key, OR +3. Use "Usuń" (Delete) button in properties panel + +The power system designer is now fully functional with professional-grade features for electrical system design and analysis! diff --git a/components/ui/Layout.js b/components/ui/Layout.js index cd582b5..d196442 100644 --- a/components/ui/Layout.js +++ b/components/ui/Layout.js @@ -11,13 +11,15 @@ import { XMarkIcon as XIcon, UserIcon, ArrowRightOnRectangleIcon as LogoutIcon, - TruckIcon + TruckIcon, + CpuChipIcon } from '@heroicons/react/24/outline'; const navigationItems = [ { name: 'Przekrój terenu', href: '/', icon: HomeIcon }, { name: 'Siatka', href: '/cross', icon: GridIcon }, { name: 'Uziomy', href: '/uziomy', icon: LightningBoltIcon }, + { name: 'Spadki napięcia', href: '/spadki', icon: CpuChipIcon }, { name: 'Śledzenie przesyłek', href: '/tracking', icon: TruckIcon }, ]; diff --git a/pages/api/spadki.js b/pages/api/spadki.js new file mode 100644 index 0000000..941c698 --- /dev/null +++ b/pages/api/spadki.js @@ -0,0 +1,191 @@ +import { getSession } from 'next-auth/react'; + +export default async function handler(req, res) { + // Authentication check + const session = await getSession({ req }); + if (!session) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { objects, cables } = req.body; + + // Validate input data + if (!objects || !cables) { + return res.status(400).json({ error: 'Missing objects or cables data' }); + } + + // Find transformer (source) + const transformer = objects.find(obj => obj.type === 'transformer'); + if (!transformer) { + return res.status(400).json({ error: 'No transformer found in the system' }); + } + + // Calculate voltage drops for each path + const results = calculateVoltageDrops(transformer, objects, cables); + + res.status(200).json({ + success: true, + data: results, + timestamp: new Date().toISOString() + }); + } catch (error) { + console.error('Voltage drop calculation error:', error); + res.status(500).json({ + error: 'Internal server error', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }); + } +} + +function calculateVoltageDrops(transformer, objects, cables) { + const results = { + transformer: transformer, + paths: [], + summary: { + totalPower: 0, + maxVoltageDrop: 0, + minVoltage: transformer.bottomVoltage + } + }; + + // Cable resistance values (Ω/km) - simplified values for common cable types + const cableResistance = { + '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 + } + }; + + // Find all paths from transformer to end connections + const paths = findAllPaths(transformer, objects, cables); + + paths.forEach((path, index) => { + let totalVoltageDrop = 0; + let pathPower = 0; + const pathDetails = []; + + // Calculate for each segment in the path + for (let i = 0; i < path.length - 1; i++) { + const fromNode = path[i]; + const toNode = path[i + 1]; + + // Find the cable connecting these nodes + const cable = cables.find(c => + (c.from === fromNode.id && c.to === toNode.id) || + (c.from === toNode.id && c.to === fromNode.id) + ); + + if (cable) { + // Get cable resistance + const resistance = getCableResistance(cable, cableResistance); + + // Calculate current (simplified - assuming balanced load) + const nodePower = toNode.powerRating || 0; + const current = nodePower > 0 ? (nodePower * 1000) / (Math.sqrt(3) * transformer.bottomVoltage * 0.9) : 0; // cos φ = 0.9 + + // Calculate voltage drop for this segment + const segmentVoltageDrop = Math.sqrt(3) * current * resistance * (cable.length / 1000); // length in km + + totalVoltageDrop += segmentVoltageDrop; + pathPower += nodePower; + + pathDetails.push({ + from: fromNode.name, + to: toNode.name, + cable: { + label: cable.label, + type: cable.cableType, + crossSection: cable.crossSection, + length: cable.length + }, + current: current.toFixed(2), + resistance: resistance.toFixed(4), + voltageDrop: segmentVoltageDrop.toFixed(2), + power: nodePower + }); + } + } + + const finalVoltage = transformer.bottomVoltage - totalVoltageDrop; + const voltageDropPercentage = (totalVoltageDrop / transformer.bottomVoltage) * 100; + + results.paths.push({ + id: index + 1, + endNode: path[path.length - 1].name, + totalVoltageDrop: totalVoltageDrop.toFixed(2), + finalVoltage: finalVoltage.toFixed(2), + voltageDropPercentage: voltageDropPercentage.toFixed(2), + pathPower: pathPower, + segments: pathDetails, + isValid: voltageDropPercentage <= 5 // 5% is typical limit + }); + + results.summary.totalPower += pathPower; + if (totalVoltageDrop > results.summary.maxVoltageDrop) { + results.summary.maxVoltageDrop = totalVoltageDrop; + results.summary.minVoltage = finalVoltage; + } + }); + + return results; +} + +function findAllPaths(transformer, objects, cables) { + const paths = []; + const visited = new Set(); + + function dfs(currentNode, currentPath) { + visited.add(currentNode.id); + currentPath.push(currentNode); + + // If this is an end connection, save the path + if (currentNode.type === 'end') { + paths.push([...currentPath]); + } else { + // Find all connected nodes + const connectedCables = cables.filter(cable => + cable.from === currentNode.id || cable.to === currentNode.id + ); + + connectedCables.forEach(cable => { + const nextNodeId = cable.from === currentNode.id ? cable.to : cable.from; + const nextNode = objects.find(obj => obj.id === nextNodeId); + + if (nextNode && !visited.has(nextNodeId)) { + dfs(nextNode, currentPath); + } + }); + } + + currentPath.pop(); + visited.delete(currentNode.id); + } + + dfs(transformer, []); + return paths; +} + +function getCableResistance(cable, resistanceTable) { + const cableType = cable.cableType || 'YAKY'; + const crossSection = cable.crossSection || 25; + + if (resistanceTable[cableType] && resistanceTable[cableType][crossSection]) { + return resistanceTable[cableType][crossSection]; + } + + // Default resistance if not found + return 1.0; +} diff --git a/pages/spadki.js b/pages/spadki.js new file mode 100644 index 0000000..f8c8f8e --- /dev/null +++ b/pages/spadki.js @@ -0,0 +1,1877 @@ +import { useState, useEffect, useRef } from "react"; +import { useSession, signIn } from "next-auth/react"; +import Layout from "../components/ui/Layout"; +import { Card, CardHeader, CardContent, CardTitle, CardDescription, Button, Alert } from "../components/ui/components"; +import { BoltIcon } from '@heroicons/react/24/outline'; +import axios from 'axios'; + +export default function Spadki() { + const { data: session } = useSession(); + const canvasRef = useRef(null); + const [isLoading, setIsLoading] = useState(false); + const [selectedTool, setSelectedTool] = useState('select'); + const [selectedObject, setSelectedObject] = useState(null); + const [selectedCable, setSelectedCable] = useState(null); + const [objects, setObjects] = useState([]); + const [cables, setCables] = useState([]); + const [connectionFrom, setConnectionFrom] = useState(null); + const [calculationResults, setCalculationResults] = useState(null); + const [error, setError] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); + const [showTooltip, setShowTooltip] = useState(false); + const [tooltipContent, setTooltipContent] = useState(''); + const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); + + // Authentication check + if (!session) { + return ( + +
+ + +

Wymagane logowanie

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

+ + Projektant Systemów Energetycznych +

+

+ Narzędzie do projektowania systemów dystrybucji energii elektrycznej +

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

+ Wybrano: {connectionFrom.name} +

+

+ Kliknij drugi obiekt aby połączyć kablem +

+
+ )} +
+ + {/* Properties Panel */} +
+

Właściwości

+ + {selectedObject ? ( +
+
+

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

+

ID: {selectedObject.id}

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

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

+

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

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

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

+

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

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

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

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