diff --git a/js/kundenkarte.js b/js/kundenkarte.js old mode 100755 new mode 100644 index 2dc0493..dcc383c --- a/js/kundenkarte.js +++ b/js/kundenkarte.js @@ -464,7 +464,7 @@ // Get tooltip data from data attribute (faster than AJAX) var tooltipDataStr = $item.attr('data-tooltip'); if (!tooltipDataStr) { - console.log('No tooltip data for anlage', anlageId); + // No tooltip data available - silently return return; } @@ -770,7 +770,7 @@ var field = data.fields[key]; // Handle header fields as section titles (must span both grid columns) if (field.type === 'header') { - html += '' + this.escapeHtml(field.label) + ''; + html += '' + this.escapeHtml(field.label) + ''; } else if (field.value) { html += '' + this.escapeHtml(field.label) + ':'; html += '' + this.escapeHtml(field.value) + ''; @@ -3836,9 +3836,11 @@ 'L3': '#808080', // Gray 'N': '#0066cc', // Blue 'PE': '#27ae60', // Green-Yellow - 'L1N': '#3498db', // Combined + 'LN': '#8B4513', // Single phase output (brown - phase determined by busbar) + 'L1N': '#3498db', // L1 + N input (legacy/busbar) '3P': '#2c3e50', // 3-phase - '3P+N': '#34495e' // 3-phase + neutral + '3P+N': '#34495e', // 3-phase + neutral + 'DATA': '#9b59b6' // Data cable (purple) }, // State @@ -3850,11 +3852,19 @@ equipment: [], dragState: null, selectedConnection: null, + DEBUG: false, // Set to true to enable debug logging + + // Conditional logging + log: function() { + if (this.DEBUG && console && console.log) { + console.log.apply(console, arguments); + } + }, init: function(anlageId) { - console.log('ConnectionEditor.init called with anlageId:', anlageId); + this.log('ConnectionEditor.init called with anlageId:', anlageId); if (!anlageId) { - console.log('ConnectionEditor: No anlageId, aborting'); + this.log('ConnectionEditor: No anlageId, aborting'); return; } this.currentAnlageId = anlageId; @@ -3862,13 +3872,13 @@ // Restore expanded state from localStorage var savedState = localStorage.getItem('kundenkarte_connection_editor_expanded'); this.isExpanded = savedState === 'true'; - console.log('ConnectionEditor: isExpanded =', this.isExpanded); + this.log('ConnectionEditor: isExpanded =', this.isExpanded); this.bindEvents(); - console.log('ConnectionEditor: Events bound'); + this.log('ConnectionEditor: Events bound'); this.renderAllEditors(); - console.log('ConnectionEditor: Editors rendered'); + this.log('ConnectionEditor: Editors rendered'); }, bindEvents: function() { @@ -5138,17 +5148,19 @@ // Constants (40% larger for better visibility) TE_WIDTH: 56, RAIL_HEIGHT: 14, // Hutschiene Höhe - RAIL_SPACING: 308, // Abstand zwischen Hutschienen (Platz für Blöcke + Verbindungen) + RAIL_SPACING: 380, // Abstand zwischen Hutschienen (erhöht für Routing-Zone) BLOCK_HEIGHT: 112, TERMINAL_RADIUS: 8, GRID_SIZE: 14, MIN_TOP_MARGIN: 100, // Minimaler Platz oben für Panel (inkl. Hauptsammelschienen) MAIN_BUSBAR_HEIGHT: 60, // Höhe des Hauptsammelschienen-Bereichs (L1,L2,L3,N,PE) BLOCK_INNER_OFFSET: 80, // Zusätzlicher Offset für Blöcke innerhalb des Panels - BOTTOM_MARGIN: 80, // Basis-Platz unten (wird dynamisch erweitert) + BOTTOM_MARGIN: 100, // Platz unten für Abgänge + Routing PANEL_GAP: 56, // Abstand zwischen Panels PANEL_PADDING: 28, // Innenabstand Panel CONNECTION_ROUTE_SPACE: 17, // Platz pro Verbindungsroute + OUTPUT_ZONE_HEIGHT: 130, // Platz für Abgänge (lineLength max 120 + Label) + ROUTING_ZONE_HEIGHT: 60, // Dedizierte Zone für Leitungsführung zwischen Carriern // Colors COLORS: { @@ -5170,7 +5182,10 @@ 'L2': '#1a1a1a', 'L3': '#666666', 'N': '#0066cc', - 'PE': '#27ae60' + 'PE': '#27ae60', + 'LN': '#8B4513', // Single phase output (brown) + '3P+N': '#34495e', // 3-phase + neutral + 'DATA': '#9b59b6' // Data cable (purple) }, // State @@ -5187,13 +5202,202 @@ svgElement: null, scale: 1, + // Performance: Index Maps für O(1) Lookups statt O(n) find() + _equipmentById: {}, + _carrierById: {}, + _connectionById: {}, + DEBUG: false, // Set to true to enable console.log + // Manual wire drawing mode wireDrawMode: false, wireDrawPoints: [], // Array of {x, y} points for current wire wireDrawSourceEq: null, // Source equipment ID wireDrawSourceTerm: null, // Source terminal ID + wireDrawFromJunction: false, // Drawing from junction on existing wire + wireDrawJunctionConn: null, // Connection ID we're branching from + wireDrawJunctionPoint: null, // {x, y} junction point on existing wire + wireExtendMode: false, // Extending an existing connection + wireExtendConnId: null, // Connection ID being extended + wireExtendExistingPoints: [], // Existing path points of connection being extended WIRE_GRID_SIZE: 25, // Snap grid size in pixels (larger = fewer points) + // Display settings (persisted in localStorage) + displaySettings: { + phaseColors: true, // Color wires by phase (L1=brown, L2=black, L3=gray, N=blue, PE=green) + wireWidth: 'normal', // 'thin', 'normal', 'thick' + showJunctions: true, // Show dots at wire junctions + terminalStyle: 'circle' // 'circle', 'square', 'ring' + }, + + // Load display settings from localStorage + loadDisplaySettings: function() { + try { + var saved = localStorage.getItem('kundenkarte_schematic_settings'); + if (saved) { + var parsed = JSON.parse(saved); + this.displaySettings = Object.assign({}, this.displaySettings, parsed); + } + } catch (e) { + console.warn('Could not load schematic settings:', e); + } + }, + + // Save display settings to localStorage + saveDisplaySettings: function() { + try { + localStorage.setItem('kundenkarte_schematic_settings', JSON.stringify(this.displaySettings)); + } catch (e) { + console.warn('Could not save schematic settings:', e); + } + }, + + // Get wire width in pixels + getWireWidth: function() { + switch (this.displaySettings.wireWidth) { + case 'thin': return 2; + case 'thick': return 5; + default: return 3.5; + } + }, + + // Get wire color based on phase (if phaseColors enabled) + getWireColor: function(connectionType) { + if (!this.displaySettings.phaseColors) { + return '#f1c40f'; // Default yellow + } + // Extract phase from connection type + var phase = connectionType ? connectionType.toUpperCase() : ''; + if (phase.indexOf('L1') !== -1) return '#8B4513'; // Brown + if (phase.indexOf('L2') !== -1) return '#1a1a1a'; // Black + if (phase.indexOf('L3') !== -1) return '#808080'; // Gray + if (phase.indexOf('PE') !== -1) return '#00aa00'; // Green + if (phase.indexOf('N') !== -1) return '#0066cc'; // Blue + if (phase === '3P' || phase === 'L1L2L3') return '#9b59b6'; // Purple for 3-phase + return '#f1c40f'; // Default yellow + }, + + // Get terminal shape SVG based on style setting + getTerminalShape: function(radius, fillColor) { + var style = this.displaySettings.terminalStyle || 'circle'; + // Add invisible hit area (larger than visible terminal) for easier clicking + var hitAreaRadius = radius * 1.4; // 11px hit area - prevents overlap on dense terminals + var hitArea = ''; + + switch (style) { + case 'ring': + // Hollow circle + return hitArea + ''; + case 'dot': + // Smaller solid dot + return hitArea + ''; + case 'square': + // Square shape + var size = radius * 1.5; + return hitArea + ''; + case 'circle': + default: + // Default filled circle + return hitArea + ''; + } + }, + + // Show settings dialog + showSettingsDialog: function() { + var self = this; + + // Remove existing dialog + $('.schematic-settings-dialog').remove(); + + var html = '
'; + + html += '
'; + html += '

Anzeigeoptionen

'; + html += ''; + html += '
'; + + // Phase colors + html += '
'; + html += ''; + html += '
'; + + // Junction dots + html += '
'; + html += ''; + html += '
'; + + // Wire width + html += '
'; + html += ''; + html += '
'; + ['thin', 'normal', 'thick'].forEach(function(w) { + var labels = { thin: 'Dünn', normal: 'Normal', thick: 'Dick' }; + var checked = self.displaySettings.wireWidth === w ? 'checked' : ''; + html += ''; + }); + html += '
'; + html += '
'; + + // Terminal style + html += '
'; + html += ''; + html += '
'; + ['circle', 'square', 'ring'].forEach(function(s) { + var labels = { circle: '● Kreis', square: '■ Quadrat', ring: '○ Ring' }; + var checked = self.displaySettings.terminalStyle === s ? 'checked' : ''; + html += ''; + }); + html += '
'; + html += '
'; + + // Buttons + html += '
'; + html += ''; + html += '
'; + + html += '
'; + + // Overlay + html += '
'; + + $('body').append(html); + + // Event handlers + $('.settings-close, .schematic-settings-overlay').on('click', function() { + $('.schematic-settings-dialog, .schematic-settings-overlay').remove(); + }); + + $('.settings-apply').on('click', function() { + // Collect settings + self.displaySettings.phaseColors = $('.setting-phase-colors').is(':checked'); + self.displaySettings.showJunctions = $('.setting-junctions').is(':checked'); + self.displaySettings.wireWidth = $('input[name="wire-width"]:checked').val() || 'normal'; + self.displaySettings.terminalStyle = $('input[name="terminal-style"]:checked').val() || 'circle'; + + // Save and apply + self.saveDisplaySettings(); + self.render(); + + // Close dialog + $('.schematic-settings-dialog, .schematic-settings-overlay').remove(); + self.showMessage('Einstellungen gespeichert', 'success'); + }); + }, + // Default terminal configs for common types // Terminals are bidirectional - no strict input/output DEFAULT_TERMINALS: { @@ -5205,12 +5409,49 @@ 'LS3P': { terminals: [{id: 't1', label: 'L1', pos: 'top'}, {id: 't2', label: 'L2', pos: 'top'}, {id: 't3', label: 'L3', pos: 'top'}, {id: 't4', label: 'L1', pos: 'bottom'}, {id: 't5', label: 'L2', pos: 'bottom'}, {id: 't6', label: 'L3', pos: 'bottom'}] } }, + // Performance: Build index maps for O(1) lookups + buildIndexes: function() { + var self = this; + this._equipmentById = {}; + this._carrierById = {}; + this._connectionById = {}; + + this.equipment.forEach(function(eq) { + self._equipmentById[eq.id] = eq; + }); + this.carriers.forEach(function(c) { + self._carrierById[c.id] = c; + }); + this.connections.forEach(function(conn) { + self._connectionById[conn.id] = conn; + }); + }, + + // O(1) lookup helpers + getEquipmentById: function(id) { + return id ? this._equipmentById[id] : null; + }, + getCarrierById: function(id) { + return id ? this._carrierById[id] : null; + }, + getConnectionById: function(id) { + return id ? this._connectionById[id] : null; + }, + + // Conditional logging (only when DEBUG is true) + log: function() { + if (this.DEBUG && console && console.log) { + console.log.apply(console, arguments); + } + }, + init: function(anlageId) { if (!anlageId) { console.error('SchematicEditor.init: No anlageId provided!'); return; } this.anlageId = anlageId; + this.loadDisplaySettings(); this.bindEvents(); this.loadData(); }, @@ -5222,9 +5463,11 @@ $(document).off('click.terminal').on('click.terminal', '.schematic-terminal', function(e) { e.preventDefault(); e.stopPropagation(); + var $clicked = $(this); + console.log('Terminal clicked:', $clicked.data('equipment-id'), $clicked.data('terminal-id'), 'element:', this); // Only handle in manual wire draw mode if (self.wireDrawMode) { - self.handleTerminalClick($(this)); + self.handleTerminalClick($clicked); } }); @@ -5308,6 +5551,12 @@ }); }); + // Straighten connections (remove diagonals, orthogonal only) + $(document).off('click.straightenConns').on('click.straightenConns', '.schematic-straighten-connections', function(e) { + e.preventDefault(); + self.straightenConnections(); + }); + // BOM (Bill of Materials) / Stückliste $(document).off('click.bomGenerate').on('click.bomGenerate', '.schematic-bom-generate', function(e) { e.preventDefault(); @@ -5349,6 +5598,12 @@ self.zoomToFit(); }); + // Settings button + $(document).off('click.schematicSettings').on('click.schematicSettings', '.schematic-settings-btn', function(e) { + e.preventDefault(); + self.showSettingsDialog(); + }); + // ====================================== // Keyboard Shortcuts // ====================================== @@ -5645,7 +5900,7 @@ self.wireDrawPoints.push({x: snapped.x, y: snapped.y}); self.updateWirePreview(); - console.log('Wire point added:', snapped.x, snapped.y, 'Total points:', self.wireDrawPoints.length); + self.log('Wire point added:', snapped.x, snapped.y, 'Total points:', self.wireDrawPoints.length); }); // SVG mousemove for wire preview - show cursor and preview line @@ -5669,15 +5924,15 @@ $(document).off('contextmenu.wireDraw').on('contextmenu.wireDraw', '.schematic-editor-canvas svg', function(e) { if (!self.wireDrawMode) return; e.preventDefault(); - if (self.wireDrawSourceEq) { - // Cancel entire drawing + if (self.wireDrawSourceEq || self.wireDrawFromJunction) { + // Cancel entire drawing (from terminal or junction) self.cancelWireDrawing(); } }); // Escape to cancel wire drawing $(document).off('keydown.wireDraw').on('keydown.wireDraw', function(e) { - if (e.key === 'Escape' && self.wireDrawMode && self.wireDrawSourceEq) { + if (e.key === 'Escape' && self.wireDrawMode && (self.wireDrawSourceEq || self.wireDrawFromJunction)) { self.cancelWireDrawing(); } }); @@ -6286,6 +6541,9 @@ var checkComplete = function() { if (connectionsLoaded && bridgesLoaded) { + // Build index maps for O(1) lookups (performance optimization) + self.buildIndexes(); + // Initialize canvas now that all data is loaded if (!self.isInitialized) { self.initCanvas(); @@ -6294,6 +6552,12 @@ } // Reset loading flag self.isLoading = false; + + // Refresh wire grid if wire draw mode is active + // (equipment positions may have changed after render) + if (self.wireDrawMode) { + self.showWireGrid(); + } } }; @@ -6346,18 +6610,12 @@ calculateLayout: function() { var self = this; - // Calculate dynamic TOP_MARGIN based on number of connections going up - // Count connections that route above blocks (top-to-top connections) - var topConnections = 0; - this.connections.forEach(function(conn) { - if (parseInt(conn.is_rail) !== 1 && conn.fk_source && conn.fk_target) { - topConnections++; - } - }); - // Panel top margin (where the panel border starts) + // Use FIXED top margin to prevent layout shifts when connections change + // This ensures path_data coordinates remain valid after saving + // Allocate space for up to 10 routing lanes (enough for most schematics) + var maxRoutingLanes = 10; this.panelTopMargin = this.MIN_TOP_MARGIN; - // Block top margin = panel margin + inner offset + connection routing space - this.calculatedTopMargin = this.panelTopMargin + this.BLOCK_INNER_OFFSET + topConnections * this.CONNECTION_ROUTE_SPACE; + this.calculatedTopMargin = this.panelTopMargin + this.BLOCK_INNER_OFFSET + maxRoutingLanes * this.CONNECTION_ROUTE_SPACE; // Calculate canvas size based on panels // Panels nebeneinander, Hutschienen pro Panel untereinander @@ -6654,12 +6912,13 @@ var terminalHtml = ''; this.equipment.forEach(function(eq) { - var carrier = self.carriers.find(function(c) { return String(c.id) === String(eq.carrier_id); }); + // Use index lookup O(1) instead of find() O(n) + var carrier = self.getCarrierById(eq.carrier_id); if (!carrier) { return; } if (typeof carrier._x === 'undefined' || carrier._x === null) { - console.log(' Equipment #' + eq.id + ' (' + eq.label + '): Carrier has no _x position set, skipping'); + self.log(' Equipment #' + eq.id + ' (' + eq.label + '): Carrier has no _x position set, skipping'); return; } @@ -6830,7 +7089,7 @@ terminalHtml += 'data-equipment-id="' + eq.id + '" data-terminal-id="' + term.id + '" '; terminalHtml += 'transform="translate(' + tx + ',' + ty + ')">'; - terminalHtml += ''; + terminalHtml += self.getTerminalShape(self.TERMINAL_RADIUS, terminalColor); // Label - position depends on whether there are stacked terminals var labelText = self.escapeHtml(term.label); @@ -6874,7 +7133,7 @@ terminalHtml += 'data-equipment-id="' + eq.id + '" data-terminal-id="' + term.id + '" '; terminalHtml += 'transform="translate(' + tx + ',' + ty + ')">'; - terminalHtml += ''; + terminalHtml += self.getTerminalShape(self.TERMINAL_RADIUS, terminalColor); // Label - position depends on whether there are stacked terminals var labelText = self.escapeHtml(term.label); @@ -7010,13 +7269,11 @@ return; } - // Get carrier for this busbar - var carrier = self.carriers.find(function(c) { - return String(c.id) === String(conn.fk_carrier); - }); + // Get carrier for this busbar (O(1) lookup) + var carrier = self.getCarrierById(conn.fk_carrier); if (!carrier || typeof carrier._x === 'undefined') { - console.log(' Busbar #' + conn.id + ': No carrier found or carrier has no position'); + self.log(' Busbar #' + conn.id + ': No carrier found or carrier has no position'); return; } @@ -7151,7 +7408,7 @@ html += ''; renderedCount++; - console.log(' Busbar #' + conn.id + ': Rendered from TE ' + startTE + ' to ' + endTE + ' on carrier ' + carrier.id + + self.log(' Busbar #' + conn.id + ': Rendered from TE ' + startTE + ' to ' + endTE + ' on carrier ' + carrier.id + ', x=' + startX + ', y=' + busbarY + ', width=' + width + ', color=' + color); }); @@ -7166,25 +7423,33 @@ var html = ''; var renderedCount = 0; + // Get display settings + var wireWidth = this.getWireWidth(); + var shadowWidth = wireWidth + 4; + var hoverWidth = wireWidth + 1.5; + this.connections.forEach(function(conn, connIndex) { // Check is_rail as integer (PHP may return string "1" or "0") if (parseInt(conn.is_rail) === 1) { return; } - var sourceEq = conn.fk_source ? self.equipment.find(function(e) { return String(e.id) === String(conn.fk_source); }) : null; - var targetEq = conn.fk_target ? self.equipment.find(function(e) { return String(e.id) === String(conn.fk_target); }) : null; + // Use index lookup O(1) instead of find() O(n) + var sourceEq = self.getEquipmentById(conn.fk_source); + var targetEq = self.getEquipmentById(conn.fk_target); - var color = conn.color || self.PHASE_COLORS[conn.connection_type] || self.COLORS.connection; + var color = conn.color || self.getWireColor(conn.connection_type); // ======================================== // ABGANG (Output) - source exists, no target // Direction depends on terminal: top terminal = line goes UP, bottom terminal = line goes DOWN + // BUT: If path_data exists, render as normal wire with that path (junction to another wire) // ======================================== - if (sourceEq && !conn.fk_target) { + if (sourceEq && !conn.fk_target && !conn.path_data) { var sourceTerminals = self.getTerminals(sourceEq); var sourceTermId = conn.source_terminal_id || 't2'; var sourcePos = self.getTerminalPosition(sourceEq, sourceTermId, sourceTerminals); + var isBundled = conn.bundled_terminals === 'all'; if (!sourcePos) { // Fallback: center bottom of equipment @@ -7195,6 +7460,26 @@ }; } + // For bundled terminals: calculate span across all terminals with same position + var bundleWidth = 0; + var bundleCenterX = sourcePos.x; + if (isBundled && sourceTerminals && sourceTerminals.length > 1) { + // Find all terminals with same position (top/bottom) + var samePosTerm = sourceTerminals.filter(function(t) { + return t.pos === (sourcePos.isTop ? 'top' : 'bottom'); + }); + if (samePosTerm.length > 1) { + var positions = samePosTerm.map(function(t) { + var pos = self.getTerminalPosition(sourceEq, t.id, sourceTerminals); + return pos ? pos.x : bundleCenterX; + }); + var minX = Math.min.apply(null, positions); + var maxX = Math.max.apply(null, positions); + bundleWidth = maxX - minX; + bundleCenterX = (minX + maxX) / 2; + } + } + // Calculate line length based on label text var labelText = conn.output_label || ''; var cableText = (conn.medium_type || '') + ' ' + (conn.medium_spec || ''); @@ -7206,26 +7491,39 @@ var startY = sourcePos.y; var endY = goingUp ? (startY - lineLength) : (startY + lineLength); - // Draw vertical line - var path = 'M ' + sourcePos.x + ' ' + startY + ' L ' + sourcePos.x + ' ' + endY; + // Use bundle center for bundled connections + var lineX = isBundled && bundleWidth > 0 ? bundleCenterX : sourcePos.x; - html += ''; + // Draw vertical line + var path = 'M ' + lineX + ' ' + startY + ' L ' + lineX + ' ' + endY; + + html += ''; + + // For bundled: draw horizontal bar connecting all terminals + if (isBundled && bundleWidth > 0) { + var barY = startY + (goingUp ? -5 : 5); + html += ''; + } // Invisible hit area for clicking var hitY = goingUp ? endY : startY; - html += ''; + var hitWidth = isBundled && bundleWidth > 0 ? Math.max(40, bundleWidth + 20) : 40; + html += ''; // Connection line html += ''; + html += 'fill="none" stroke="' + color + '" stroke-width="' + wireWidth + '" stroke-linecap="round"/>'; // Arrow at end (pointing away from equipment) + var arrowSize = isBundled && bundleWidth > 0 ? 8 : 5; if (goingUp) { // Arrow pointing UP - html += ''; + html += ''; } else { // Arrow pointing DOWN - html += ''; + html += ''; } // Labels - vertical text on both sides @@ -7233,9 +7531,9 @@ // Left side: Bezeichnung (output_label) if (conn.output_label) { - html += ''; + html += 'transform="rotate(-90 ' + (lineX - 10) + ' ' + labelY + ')">'; html += self.escapeHtml(conn.output_label); html += ''; } @@ -7245,9 +7543,9 @@ if (conn.medium_type) cableInfo = conn.medium_type; if (conn.medium_spec) cableInfo += ' ' + conn.medium_spec; if (cableInfo) { - html += ''; + html += 'transform="rotate(-90 ' + (lineX + 10) + ' ' + labelY + ')">'; html += self.escapeHtml(cableInfo.trim()); html += ''; } @@ -7255,7 +7553,7 @@ // Phase type at end of line if (conn.connection_type) { var phaseY = goingUp ? (endY - 10) : (endY + 14); - html += ''; html += conn.connection_type; html += ''; @@ -7267,7 +7565,67 @@ } // ======================================== - // ANSCHLUSSPUNKT (Input) - no source, target exists + // JUNCTION CONNECTION - branch from existing wire + // No source equipment but has path_data (manually drawn from junction point) + // ======================================== + if (!conn.fk_source && targetEq && conn.path_data) { + var targetTerminals = self.getTerminals(targetEq); + var targetTermId = conn.target_terminal_id || 't1'; + var targetPos = self.getTerminalPosition(targetEq, targetTermId, targetTerminals); + if (!targetPos) return; + + // Use the manually drawn path + var path = conn.path_data; + + // Junction connections use phase colors + var junctionColor = color; + + // Extract first point from path for junction marker + var pathMatch = path.match(/M\s*([\d.-]+)\s*([\d.-]+)/); + var junctionX = pathMatch ? parseFloat(pathMatch[1]) : 0; + var junctionY = pathMatch ? parseFloat(pathMatch[2]) : 0; + + html += ''; + + html += ''; + html += ''; + + // Junction marker (dot at start point) + if (self.displaySettings.showJunctions) { + html += ''; + } + + // Label if present + if (conn.output_label) { + var labelWidth = Math.min(conn.output_label.length * 8 + 16, 120); + var labelHeight = 22; + + // Find safe position that doesn't overlap equipment + var labelPos = self.findSafeLabelPosition(path, labelWidth, labelHeight); + var labelX = labelPos ? labelPos.x : (junctionX + targetPos.x) / 2; + var labelY = labelPos ? labelPos.y : (junctionY + targetPos.y) / 2; + + // Badge with solid background and border for visibility + // Text always white for readability (e.g. L2 black wire) + html += ''; + html += ''; + html += self.escapeHtml(conn.output_label); + html += ''; + } + + html += ''; + renderedCount++; + return; + } + + // ======================================== + // ANSCHLUSSPUNKT (Input) - no source, target exists, NO path_data // Draw a line coming FROM ABOVE into the terminal // All Anschlusspunkte use a uniform color (light blue) // ======================================== @@ -7295,7 +7653,7 @@ // Connection line html += ''; + html += 'fill="none" stroke="' + inputColor + '" stroke-width="' + wireWidth + '" stroke-linecap="round"/>'; // Circle at top (external source indicator) html += ''; @@ -7324,6 +7682,53 @@ return; } + // ======================================== + // LEITUNG ZU LEITUNG - source exists, no target, but has path_data + // Wire ends at another wire (junction), rendered with stored path + // ======================================== + if (sourceEq && !conn.fk_target && conn.path_data) { + // Use the manually drawn path + var path = conn.path_data; + + // Extract last point for junction marker + var pathPoints = path.match(/[ML]\s*([\d.-]+)\s*([\d.-]+)/g); + var lastMatch = pathPoints && pathPoints.length > 0 ? pathPoints[pathPoints.length - 1].match(/([\d.-]+)\s*([\d.-]+)/) : null; + var junctionX = lastMatch ? parseFloat(lastMatch[1]) : 0; + var junctionY = lastMatch ? parseFloat(lastMatch[2]) : 0; + + html += ''; + + html += ''; + html += ''; + + // Junction marker (dot at end point where it connects to other wire) + html += ''; + + // Label if present + if (conn.output_label) { + var labelWidth = Math.min(conn.output_label.length * 8 + 16, 120); + var labelHeight = 22; + var labelPos = self.findSafeLabelPosition(path, labelWidth, labelHeight); + var labelX = labelPos ? labelPos.x : junctionX; + var labelY = labelPos ? labelPos.y : junctionY - 20; + + html += ''; + html += ''; + html += self.escapeHtml(conn.output_label); + html += ''; + } + + html += ''; + renderedCount++; + return; + } + // ======================================== // NORMAL CONNECTION - both source and target exist // ======================================== @@ -7351,30 +7756,52 @@ path = self.createOrthogonalPath(sourcePos, targetPos, routeOffset, sourceEq, targetEq); } - html += ''; + html += ''; html += ''; html += ''; + html += 'fill="none" stroke="' + color + '" stroke-width="' + wireWidth + '" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"/>'; if (conn.output_label) { - var labelX = (sourcePos.x + targetPos.x) / 2; - var labelY = sourcePos.isTop ? sourcePos.y - 70 - routeOffset : sourcePos.y + 70 + routeOffset; + // Calculate label dimensions + var labelWidth = Math.min(conn.output_label.length * 8 + 16, 120); + var labelHeight = 22; - var labelWidth = Math.min(conn.output_label.length * 8 + 14, 112); - html += ''; - html += ''; + // Find safe label position that doesn't overlap equipment + var labelPos = self.findSafeLabelPosition(path, labelWidth, labelHeight); + if (!labelPos) { + labelPos = self.getPathMidpoint(path); + } + var labelX = labelPos ? labelPos.x : (sourcePos.x + targetPos.x) / 2; + var labelY = labelPos ? labelPos.y : (sourcePos.y + targetPos.y) / 2; + + // Badge with solid background and border for visibility + // Text always white for readability (e.g. L2 black wire) + html += ''; + html += ''; html += self.escapeHtml(conn.output_label); html += ''; } if (conn.connection_type && !conn.output_label) { - var typeX = (sourcePos.x + targetPos.x) / 2; - var typeY = (sourcePos.y + targetPos.y) / 2; - html += ''; + // Also check for safe position for type labels + var typeWidth = conn.connection_type.length * 9 + 14; + var typeHeight = 18; + var typePos = self.findSafeLabelPosition(path, typeWidth, typeHeight); + var typeX = typePos ? typePos.x : (sourcePos.x + targetPos.x) / 2; + var typeY = typePos ? typePos.y : (sourcePos.y + targetPos.y) / 2; + + // Badge background for type label too + // Text always white for readability + html += ''; + html += ''; html += conn.connection_type; html += ''; } @@ -7397,21 +7824,78 @@ this.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); + + // If we're in wire draw mode with a source terminal selected, + // create a junction: connection ends at the click point on the wire + if (self.wireDrawMode && self.wireDrawSourceEq && connId) { + var conn = self.connections.find(function(c) { return c.id == connId; }); + if (conn) { + // Get click position relative to SVG + var $svg = $(e.target).closest('svg'); + var svgRect = $svg[0].getBoundingClientRect(); + var rawClickX = e.clientX - svgRect.left; + var rawClickY = e.clientY - svgRect.top; + + // Snap click point to the nearest point on the wire + var snappedPoint = self.snapToWirePath(conn.path_data, rawClickX, rawClickY); + var clickX = Math.round(snappedPoint.x); + var clickY = Math.round(snappedPoint.y); + + var sourceEqId = self.wireDrawSourceEq; + var sourceTermId = self.wireDrawSourceTerm; + + // Build path: source terminal → existing points → click point on wire + var pathPoints = self.wireDrawPoints.slice(); + + // Add click point with orthogonal correction + if (pathPoints.length > 0) { + var lastPt = pathPoints[pathPoints.length - 1]; + if (lastPt.x !== clickX && lastPt.y !== clickY) { + // Insert intermediate point for 90° bend + pathPoints.push({x: lastPt.x, y: clickY}); + } + } + pathPoints.push({x: clickX, y: clickY}); + + // Remove redundant points (points on same line as neighbors) + pathPoints = self.cleanupPathPoints(pathPoints); + + // Build path string + var pathData = ''; + for (var i = 0; i < pathPoints.length; i++) { + var pt = pathPoints[i]; + pathData += (i === 0 ? 'M ' : 'L ') + pt.x + ' ' + pt.y + ' '; + } + + // Store connection values to inherit from clicked wire + self._inheritFromConnection = conn; + self._pendingPathData = pathData.trim() || null; + + // Clear source terminal but keep draw mode active + self.cleanupWireDrawState(true); + + // Leitung endet an der angeklickten Leitung (kein Ziel-Terminal) + self.showConnectionLabelDialog(sourceEqId, sourceTermId, null, null); + return; + } + } + + // Normal click: show popup if (connId) { self.showConnectionPopup(connId, e.clientX, e.clientY); } }); this.addEventListener('mouseenter', function() { - $visiblePath.attr('stroke-width', '5'); + $visiblePath.attr('stroke-width', hoverWidth); }); this.addEventListener('mouseleave', function() { - $visiblePath.attr('stroke-width', '3.5'); + $visiblePath.attr('stroke-width', wireWidth); }); this.style.cursor = 'pointer'; }); - // Abgang (Output) groups - click to edit + // Abgang (Output) groups - click to edit or junction $layer.find('.schematic-output-group').each(function() { var $group = $(this); var connId = $group.data('connection-id'); @@ -7420,25 +7904,142 @@ this.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); + + // If in wire draw mode with source terminal, create junction at click point + if (self.wireDrawMode && self.wireDrawSourceEq && connId) { + var conn = self.connections.find(function(c) { return c.id == connId; }); + if (conn) { + // Get click position relative to SVG + var $svg = $(e.target).closest('svg'); + var svgRect = $svg[0].getBoundingClientRect(); + var rawClickX = e.clientX - svgRect.left; + var rawClickY = e.clientY - svgRect.top; + + // Abgänge are vertical lines - snap X to the line's X coordinate + // Get source equipment position for the output + var outputEq = self.equipment.find(function(eq) { return eq.id == conn.fk_source; }); + var clickX, clickY; + if (outputEq && conn.source_terminal_id) { + var terminals = self.getTerminals(outputEq); + var termPos = self.getTerminalPosition(outputEq, conn.source_terminal_id, terminals); + if (termPos) { + // Snap X to terminal X (vertical line), keep Y from click + clickX = Math.round(termPos.x); + clickY = Math.round(rawClickY); + } else { + clickX = Math.round(rawClickX); + clickY = Math.round(rawClickY); + } + } else { + clickX = Math.round(rawClickX); + clickY = Math.round(rawClickY); + } + + var sourceEqId = self.wireDrawSourceEq; + var sourceTermId = self.wireDrawSourceTerm; + + // Build path ending at click point + var pathPoints = self.wireDrawPoints.slice(); + if (pathPoints.length > 0) { + var lastPt = pathPoints[pathPoints.length - 1]; + if (lastPt.x !== clickX && lastPt.y !== clickY) { + pathPoints.push({x: lastPt.x, y: clickY}); + } + } + pathPoints.push({x: clickX, y: clickY}); + + // Remove redundant points + pathPoints = self.cleanupPathPoints(pathPoints); + + var pathData = ''; + for (var i = 0; i < pathPoints.length; i++) { + var pt = pathPoints[i]; + pathData += (i === 0 ? 'M ' : 'L ') + pt.x + ' ' + pt.y + ' '; + } + + self._inheritFromConnection = conn; + self._pendingPathData = pathData.trim() || null; + self.cleanupWireDrawState(true); + // Leitung endet an Abgang + self.showConnectionLabelDialog(sourceEqId, sourceTermId, null, null); + return; + } + } + if (connId) { self.showConnectionPopup(connId, e.clientX, e.clientY); } }); this.addEventListener('mouseenter', function() { - $visiblePath.attr('stroke-width', '5'); + $visiblePath.attr('stroke-width', hoverWidth); }); this.addEventListener('mouseleave', function() { - $visiblePath.attr('stroke-width', '3'); + $visiblePath.attr('stroke-width', wireWidth); }); }); - // Anschlusspunkt (Input) groups - click to edit + // Anschlusspunkt (Input) groups - click to edit or junction $layer.find('.schematic-input-group').each(function() { var $group = $(this); var connId = $group.data('connection-id'); var $visiblePath = $group.find('.schematic-connection'); + this.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + // If in wire draw mode with source terminal, connect to input's target + if (self.wireDrawMode && self.wireDrawSourceEq && connId) { + var conn = self.connections.find(function(c) { return c.id == connId; }); + if (conn && conn.fk_target) { + var sourceEqId = self.wireDrawSourceEq; + var sourceTermId = self.wireDrawSourceTerm; + var targetEqId = conn.fk_target; + var targetTermId = conn.target_terminal_id || 'input'; + + // Build path with existing points + var pathPoints = self.wireDrawPoints.slice(); + var targetEq = self.equipment.find(function(eq) { return eq.id == targetEqId; }); + if (targetEq) { + var terminals = self.getTerminals(targetEq); + var targetPos = self.getTerminalPosition(targetEq, targetTermId, terminals); + if (targetPos) { + pathPoints.push({x: targetPos.x, y: targetPos.y}); + } + } + var pathData = ''; + for (var i = 0; i < pathPoints.length; i++) { + pathData += (i === 0 ? 'M' : 'L') + ' ' + pathPoints[i].x + ' ' + pathPoints[i].y + ' '; + } + + self._inheritFromConnection = conn; + self._pendingPathData = pathData.trim() || null; + self.cleanupWireDrawState(true); + self.showConnectionLabelDialog(sourceEqId, sourceTermId, targetEqId, targetTermId); + return; + } + } + + if (connId) { + self.showConnectionPopup(connId, e.clientX, e.clientY); + } + }); + + this.addEventListener('mouseenter', function() { + $visiblePath.attr('stroke-width', hoverWidth); + }); + this.addEventListener('mouseleave', function() { + $visiblePath.attr('stroke-width', wireWidth); + }); + }); + + // Junction (Branch) groups - click to edit + $layer.find('.schematic-junction-group').each(function() { + var $group = $(this); + var connId = $group.data('connection-id'); + var $visiblePath = $group.find('.schematic-connection'); + this.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); @@ -7448,11 +8049,12 @@ }); this.addEventListener('mouseenter', function() { - $visiblePath.attr('stroke-width', '5'); + $visiblePath.attr('stroke-width', hoverWidth); }); this.addEventListener('mouseleave', function() { - $visiblePath.attr('stroke-width', '3'); + $visiblePath.attr('stroke-width', wireWidth); }); + this.style.cursor = 'pointer'; }); }, @@ -7589,7 +8191,7 @@ var $addBusbar = $(''); $controls.append($addBusbar); } else { - console.log('Carrier ' + carrier.id + ' (' + carrier.label + '): Missing _x or _y position'); + self.log('Carrier ' + carrier.id + ' (' + carrier.label + '): Missing _x or _y position'); } }); @@ -8572,6 +9174,12 @@ return this.DEFAULT_TERMINALS['LS'].terminals; }, + // Get total terminal count for equipment (used for bundled terminals feature) + getTerminalCount: function(eq) { + var terminals = this.getTerminals(eq); + return terminals ? terminals.length : 2; + }, + getTerminalPosition: function(eq, terminalId, terminals) { // Find the terminal var terminal = null; @@ -8628,10 +9236,13 @@ var widthTE = parseFloat(eq.width_te) || 1; // Terminal im festen TE-Raster platzieren - // Jeder Terminal belegt 1 TE - Index bestimmt welches TE - var teIndex = termIndex % widthTE; + // Use terminal.col if defined, otherwise fall back to index + var teIndex = terminal.col !== undefined ? terminal.col : (termIndex % widthTE); var x = eq._x + (teIndex * this.TE_WIDTH) + (this.TE_WIDTH / 2); - var y = isTop ? (eq._y - 5) : (eq._y + eq._height + 5); + + // For stacked terminals, add row offset + var rowOffset = terminal.row !== undefined ? terminal.row * 14 : 0; + var y = isTop ? (eq._y - 5 - rowOffset) : (eq._y + eq._height + 5 + rowOffset); return { x: x, y: y, isTop: isTop }; }, @@ -8657,6 +9268,270 @@ return this.createSimpleRoute(x1, y1, x2, y2, source.isTop, target.isTop, routeOffset); }, + // Remove redundant points that lie on the same line as their neighbors + cleanupPathPoints: function(points) { + if (points.length < 3) return points; + + var cleaned = [points[0]]; + + for (var i = 1; i < points.length - 1; i++) { + var prev = cleaned[cleaned.length - 1]; + var curr = points[i]; + var next = points[i + 1]; + + // Check if curr is on the same line as prev and next + var sameHorizontal = (prev.y === curr.y && curr.y === next.y); + var sameVertical = (prev.x === curr.x && curr.x === next.x); + + // Only keep point if it's a corner (direction change) + if (!sameHorizontal && !sameVertical) { + cleaned.push(curr); + } + } + + // Always add the last point + cleaned.push(points[points.length - 1]); + + return cleaned; + }, + + // Snap a point to the nearest point on a wire path + snapToWirePath: function(pathData, clickX, clickY) { + if (!pathData) return {x: clickX, y: clickY}; + + // Parse path to get points + var points = []; + var commands = pathData.match(/[MLHVZ][^MLHVZ]*/gi); + if (!commands) return {x: clickX, y: clickY}; + + var currentX = 0, currentY = 0; + commands.forEach(function(cmd) { + var type = cmd[0].toUpperCase(); + var coords = cmd.slice(1).trim().split(/[\s,]+/).map(parseFloat); + if (type === 'M' || type === 'L') { + currentX = coords[0]; + currentY = coords[1]; + points.push({x: currentX, y: currentY}); + } else if (type === 'H') { + currentX = coords[0]; + points.push({x: currentX, y: currentY}); + } else if (type === 'V') { + currentY = coords[0]; + points.push({x: currentX, y: currentY}); + } + }); + + if (points.length < 2) return {x: clickX, y: clickY}; + + // Find nearest point on any segment + var nearestPoint = {x: clickX, y: clickY}; + var minDist = Infinity; + + for (var i = 1; i < points.length; i++) { + var p1 = points[i - 1]; + var p2 = points[i]; + + // Project click point onto line segment + var dx = p2.x - p1.x; + var dy = p2.y - p1.y; + var lenSq = dx * dx + dy * dy; + + var t = 0; + if (lenSq > 0) { + t = ((clickX - p1.x) * dx + (clickY - p1.y) * dy) / lenSq; + t = Math.max(0, Math.min(1, t)); // Clamp to segment + } + + var projX = p1.x + t * dx; + var projY = p1.y + t * dy; + + var dist = Math.sqrt((clickX - projX) * (clickX - projX) + (clickY - projY) * (clickY - projY)); + + if (dist < minDist) { + minDist = dist; + nearestPoint = {x: projX, y: projY}; + } + } + + return nearestPoint; + }, + + // Get the midpoint of an SVG path for label positioning + getPathMidpoint: function(pathData) { + if (!pathData) return null; + + // Parse path commands (M, L, H, V) + var points = []; + var currentX = 0, currentY = 0; + + // Match all path commands + var commands = pathData.match(/[MLHVZ][^MLHVZ]*/gi); + if (!commands) return null; + + commands.forEach(function(cmd) { + var type = cmd[0].toUpperCase(); + var coords = cmd.slice(1).trim().split(/[\s,]+/).map(parseFloat); + + if (type === 'M' || type === 'L') { + currentX = coords[0]; + currentY = coords[1]; + points.push({x: currentX, y: currentY}); + } else if (type === 'H') { + currentX = coords[0]; + points.push({x: currentX, y: currentY}); + } else if (type === 'V') { + currentY = coords[0]; + points.push({x: currentX, y: currentY}); + } + }); + + if (points.length < 2) return null; + + // Calculate total path length + var totalLength = 0; + var segments = []; + for (var i = 1; i < points.length; i++) { + var dx = points[i].x - points[i-1].x; + var dy = points[i].y - points[i-1].y; + var len = Math.sqrt(dx*dx + dy*dy); + segments.push({start: points[i-1], end: points[i], length: len}); + totalLength += len; + } + + // Find the point at half the total length + var halfLength = totalLength / 2; + var accumulated = 0; + + for (var j = 0; j < segments.length; j++) { + var seg = segments[j]; + if (accumulated + seg.length >= halfLength) { + // Midpoint is in this segment + var t = (halfLength - accumulated) / seg.length; + return { + x: seg.start.x + t * (seg.end.x - seg.start.x), + y: seg.start.y + t * (seg.end.y - seg.start.y) + }; + } + accumulated += seg.length; + } + + // Fallback: return geometric center + return { + x: (points[0].x + points[points.length-1].x) / 2, + y: (points[0].y + points[points.length-1].y) / 2 + }; + }, + + // Check if a label rectangle overlaps with any equipment block + labelOverlapsEquipment: function(labelX, labelY, labelWidth, labelHeight) { + var self = this; + var padding = 5; // Safety margin + + // Label bounding box + var lx1 = labelX - labelWidth / 2 - padding; + var ly1 = labelY - labelHeight / 2 - padding; + var lx2 = labelX + labelWidth / 2 + padding; + var ly2 = labelY + labelHeight / 2 + padding; + + for (var i = 0; i < this.equipment.length; i++) { + var eq = this.equipment[i]; + if (typeof eq._x === 'undefined' || typeof eq._y === 'undefined') continue; + + var eqWidth = (parseFloat(eq.width_te) || 1) * this.TE_WIDTH; + var eqHeight = eq._height || this.BLOCK_HEIGHT; + + // Equipment bounding box + var ex1 = eq._x; + var ey1 = eq._y; + var ex2 = eq._x + eqWidth; + var ey2 = eq._y + eqHeight; + + // Check for overlap + if (!(lx2 < ex1 || lx1 > ex2 || ly2 < ey1 || ly1 > ey2)) { + return eq; // Return the overlapping equipment + } + } + return null; // No overlap + }, + + // Find a safe label position along a path that doesn't overlap equipment + findSafeLabelPosition: function(pathData, labelWidth, labelHeight) { + var self = this; + if (!pathData) return null; + + // Parse path to get all points + var points = []; + var commands = pathData.match(/[ML]\s*[\d.-]+\s*[\d.-]+/gi); + if (!commands) return null; + + commands.forEach(function(cmd) { + var coords = cmd.match(/[\d.-]+/g); + if (coords && coords.length >= 2) { + points.push({ x: parseFloat(coords[0]), y: parseFloat(coords[1]) }); + } + }); + + if (points.length < 2) return null; + + // Try positions along the path (at 10%, 30%, 50%, 70%, 90%) + var percentages = [0.5, 0.3, 0.7, 0.2, 0.8, 0.1, 0.9]; + + // Calculate total path length and segment lengths + var totalLength = 0; + var segments = []; + for (var i = 1; i < points.length; i++) { + var dx = points[i].x - points[i-1].x; + var dy = points[i].y - points[i-1].y; + var len = Math.sqrt(dx*dx + dy*dy); + segments.push({ start: points[i-1], end: points[i], length: len }); + totalLength += len; + } + + // Try each percentage + for (var p = 0; p < percentages.length; p++) { + var targetLength = totalLength * percentages[p]; + var accumulated = 0; + + for (var j = 0; j < segments.length; j++) { + var seg = segments[j]; + if (accumulated + seg.length >= targetLength) { + var t = (targetLength - accumulated) / seg.length; + var testX = seg.start.x + t * (seg.end.x - seg.start.x); + var testY = seg.start.y + t * (seg.end.y - seg.start.y); + + // Check if this position is safe + if (!this.labelOverlapsEquipment(testX, testY, labelWidth, labelHeight)) { + return { x: testX, y: testY }; + } + break; + } + accumulated += seg.length; + } + } + + // All positions overlap - try to find position outside equipment + // Return the midpoint but offset away from nearest equipment + var midPos = this.getPathMidpoint(pathData); + if (!midPos) return null; + + var overlappingEq = this.labelOverlapsEquipment(midPos.x, midPos.y, labelWidth, labelHeight); + if (overlappingEq) { + // Move label away from the equipment + var eqCenterY = overlappingEq._y + (overlappingEq._height || this.BLOCK_HEIGHT) / 2; + + // Move above or below the equipment + if (midPos.y < eqCenterY) { + // Label is above center, move it further up + midPos.y = overlappingEq._y - labelHeight / 2 - 10; + } else { + // Label is below center, move it further down + midPos.y = overlappingEq._y + (overlappingEq._height || this.BLOCK_HEIGHT) + labelHeight / 2 + 10; + } + } + + return midPos; + }, + // Pathfinding-based routing with obstacle avoidance and connection spreading // Improved version using A* with better orthogonal routing createPathfindingRoute: function(x1, y1, x2, y2, connIndex) { @@ -8901,10 +9776,20 @@ handleTerminalClick: function($terminal) { var eqId = $terminal.data('equipment-id'); var termId = $terminal.data('terminal-id'); + this.log('handleTerminalClick called - eqId:', eqId, 'termId:', termId); + this.log('State: wireDrawMode:', this.wireDrawMode, 'wireExtendMode:', this.wireExtendMode, 'wireDrawSourceEq:', this.wireDrawSourceEq); // Only works in manual wire draw mode if (!this.wireDrawMode) return; + // In extend mode, ANY terminal click finishes the drawing + // (user wants to extend the wire to this terminal) + if (this.wireExtendMode) { + this.log('Extend mode: finishing wire to terminal', eqId, termId); + this.finishWireDrawing(eqId, termId); + return; + } + if (!this.wireDrawSourceEq) { // First terminal - start drawing this.wireDrawSourceEq = eqId; @@ -8922,10 +9807,13 @@ } $terminal.find('.schematic-terminal-circle').attr('stroke', '#ff0').attr('stroke-width', '3'); - this.showMessage('Rasterpunkte klicken, Rechtsklick = Abbruch, dann Ziel-Terminal klicken', 'info'); + this.showMessage('Rasterpunkte klicken, Rechtsklick/ESC = Abbruch, gleiches Terminal = Abbruch', 'info'); this.showWireGrid(); - } else if (eqId !== this.wireDrawSourceEq) { - // Second terminal - finish and save + } else if (eqId === this.wireDrawSourceEq && termId === this.wireDrawSourceTerm) { + // Same terminal clicked again - cancel drawing + this.cancelWireDrawing(); + } else { + // Different terminal - finish and save this.finishWireDrawing(eqId, termId); } }, @@ -8937,10 +9825,12 @@ if (this.wireDrawMode) { $btn.addClass('active').css('background', '#27ae60'); - this.showMessage('Manueller Zeichenmodus: Klicken Sie auf ein START-Terminal (roter Kreis)', 'info'); + this.showMessage('Manueller Zeichenmodus: Klicke Terminal oder Leitung für Startpunkt', 'info'); this.showWireGrid(); // Highlight all terminals to show they are clickable - $(this.svgElement).find('.schematic-terminal-circle').css('cursor', 'crosshair'); + $(this.svgElement).find('.schematic-terminal').css('cursor', 'crosshair'); + // Also highlight connections as clickable for junction + $(this.svgElement).find('.schematic-connection-hitarea').css('cursor', 'crosshair'); } else { $btn.removeClass('active').css('background', ''); this.cancelWireDrawing(); @@ -8949,10 +9839,329 @@ } }, + // Start drawing from a junction point on an existing connection + startJunctionDraw: function(connId, event) { + var self = this; + var conn = this.connections.find(function(c) { return String(c.id) === String(connId); }); + if (!conn) return; + + // Get click position in SVG coordinates + var svg = this.svgElement; + var pt = svg.createSVGPoint(); + pt.x = event.clientX; + pt.y = event.clientY; + var svgPt = pt.matrixTransform(svg.getScreenCTM().inverse()); + + // Find nearest point on the connection path + var junctionPoint = this.findNearestPointOnConnection(conn, svgPt.x, svgPt.y); + if (!junctionPoint) { + this.showMessage('Konnte Abzweigpunkt nicht berechnen', 'error'); + return; + } + + // Store junction info + this.wireDrawFromJunction = true; + this.wireDrawJunctionConn = connId; + this.wireDrawJunctionPoint = junctionPoint; + this.wireDrawPoints = [{x: junctionPoint.x, y: junctionPoint.y}]; + + // Visual feedback - draw a marker at junction point + this.showJunctionMarker(junctionPoint); + + this.showMessage('Abzweigung von Leitung: Rasterpunkte klicken, dann Ziel-Terminal', 'info'); + }, + + // Finish drawing a wire that ends at a junction on an existing connection + // (Terminal -> Wire junction) + finishWireToJunction: function(connId, event) { + var self = this; + var conn = this.connections.find(function(c) { return String(c.id) === String(connId); }); + if (!conn) return; + + // Get click position in SVG coordinates + var svg = this.svgElement; + var pt = svg.createSVGPoint(); + pt.x = event.clientX; + pt.y = event.clientY; + var svgPt = pt.matrixTransform(svg.getScreenCTM().inverse()); + + // Find nearest point on the connection path + var junctionPoint = this.findNearestPointOnConnection(conn, svgPt.x, svgPt.y); + if (!junctionPoint) { + this.showMessage('Konnte Verbindungspunkt nicht berechnen', 'error'); + return; + } + + // Add junction point to path + this.wireDrawPoints.push({x: junctionPoint.x, y: junctionPoint.y}); + + // Build path string from points + var pathData = ''; + for (var i = 0; i < this.wireDrawPoints.length; i++) { + pathData += (i === 0 ? 'M' : 'L') + ' ' + this.wireDrawPoints[i].x + ' ' + this.wireDrawPoints[i].y + ' '; + } + + // Show dialog to configure the connection + // Note: cleanup is done inside the dialog handlers (save/cancel) + this.showTerminalToJunctionDialog( + this.wireDrawSourceEq, + this.wireDrawSourceTerm, + connId, + junctionPoint, + pathData.trim() + ); + }, + + // Dialog for creating a connection from terminal to junction on existing wire + showTerminalToJunctionDialog: function(sourceEqId, sourceTermId, targetConnId, junctionPoint, pathData) { + var self = this; + + // Remove any existing dialog first + $('#schematic-conn-dialog').remove(); + + var sourceEq = this.equipment.find(function(e) { return String(e.id) === String(sourceEqId); }); + var targetConn = this.connections.find(function(c) { return String(c.id) === String(targetConnId); }); + + var targetLabel = targetConn ? (targetConn.output_label || targetConn.connection_type || 'Leitung') : 'Leitung'; + + var html = '
'; + html += '
'; + html += '

Verbindung zur Leitung

'; + html += '×
'; + html += '
'; + + html += '
'; + html += '' + this.escapeHtml((sourceEq ? sourceEq.label || sourceEq.type_label_short : 'Gerät')) + ''; + html += ' '; + html += ' ' + this.escapeHtml(targetLabel) + ''; + html += '
'; + + html += '
'; + html += ''; + html += ''; + html += '
'; + + html += '
'; + html += ''; + html += '
'; + + var typePresets = ['L1', 'L2', 'L3', 'N', 'PE', 'L1N', '3P']; + typePresets.forEach(function(t) { + var color = self.PHASE_COLORS[t] || self.COLORS.connection; + html += ''; + }); + + html += '
'; + html += ''; + html += ''; + html += '
'; + + html += '
'; + html += ''; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Bundled terminals checkbox - only show if source equipment has >1 terminal + var sourceTerminalCount = sourceEq ? this.getTerminalCount(sourceEq) : 1; + if (sourceTerminalCount > 1) { + html += '
'; + html += ''; + html += '
'; + } + + html += '
'; + html += '
'; + + $('body').append(html); + + // Type preset buttons + $('.conn-type-btn').on('click', function() { + $('.conn-type-btn').css('outline', 'none'); + $(this).css('outline', '2px solid #fff'); + $('#conn-type').val($(this).data('type')); + $('#conn-color').val($(this).data('color')); + }); + + $('#conn-save').on('click', function() { + var label = $('#conn-label').val().trim(); + var connType = $('#conn-type').val(); + var color = $('#conn-color').val(); + var medium = $('#conn-medium').val(); + var length = $('#conn-length').val(); + var bundleAll = $('#conn-bundle-all').is(':checked'); + + // Create connection: terminal -> junction on wire + self.saveJunctionConnection({ + anlage_id: self.anlageId, + fk_source: sourceEqId, + source_terminal_id: sourceTermId, + fk_target: null, // No target equipment + target_terminal_id: null, + output_label: label, + connection_type: connType, + color: color, + medium_type: medium, + medium_length: length, + path_data: pathData, + bundled_terminals: bundleAll ? 'all' : '', + junction_connection_id: targetConnId, + junction_x: junctionPoint.x, + junction_y: junctionPoint.y + }); + + $('#schematic-conn-dialog').remove(); + // Cleanup after dialog closes - keep draw mode active + self.cleanupWireDrawState(true); + }); + + $('#conn-cancel, .kundenkarte-modal-close').on('click', function() { + $('#schematic-conn-dialog').remove(); + // Cleanup after dialog closes - keep draw mode active + self.cleanupWireDrawState(true); + }); + }, + + // Find nearest point on a connection path + findNearestPointOnConnection: function(conn, clickX, clickY) { + var pathData = conn.path_data; + if (!pathData) { + // No manual path - calculate from terminals + var sourceEq = conn.fk_source ? this.equipment.find(function(e) { return String(e.id) === String(conn.fk_source); }) : null; + var targetEq = conn.fk_target ? this.equipment.find(function(e) { return String(e.id) === String(conn.fk_target); }) : null; + + // For outputs (no target), create path from source going down/up + if (sourceEq && !targetEq) { + var sourceTerminals = this.getTerminals(sourceEq); + var sourcePos = this.getTerminalPosition(sourceEq, conn.source_terminal_id || 't2', sourceTerminals); + if (!sourcePos) return null; + + // Create a simple vertical path for output + var direction = sourcePos.position === 'top' ? -1 : 1; + var endY = sourcePos.y + (direction * 60); + pathData = 'M ' + sourcePos.x + ' ' + sourcePos.y + ' L ' + sourcePos.x + ' ' + endY; + } else if (sourceEq && targetEq) { + var sourceTerminals = this.getTerminals(sourceEq); + var targetTerminals = this.getTerminals(targetEq); + var sourcePos = this.getTerminalPosition(sourceEq, conn.source_terminal_id || 't2', sourceTerminals); + var targetPos = this.getTerminalPosition(targetEq, conn.target_terminal_id || 't1', targetTerminals); + if (!sourcePos || !targetPos) return null; + + pathData = this.createOrthogonalPath(sourcePos, targetPos, 0, sourceEq, targetEq); + } else { + return null; + } + } + + // Parse path data to get segments + var points = this.parsePathToPoints(pathData); + if (points.length < 2) return null; + + // Find nearest point on any segment + var nearestPoint = null; + var minDist = Infinity; + + for (var i = 0; i < points.length - 1; i++) { + var p1 = points[i]; + var p2 = points[i + 1]; + var closest = this.nearestPointOnSegment(p1, p2, clickX, clickY); + var dist = Math.sqrt(Math.pow(closest.x - clickX, 2) + Math.pow(closest.y - clickY, 2)); + + if (dist < minDist) { + minDist = dist; + nearestPoint = closest; + } + } + + // Snap to grid + if (nearestPoint) { + var gridSize = 5; + nearestPoint.x = Math.round(nearestPoint.x / gridSize) * gridSize; + nearestPoint.y = Math.round(nearestPoint.y / gridSize) * gridSize; + } + + return nearestPoint; + }, + + // Parse SVG path data to array of points + parsePathToPoints: function(pathData) { + var points = []; + var commands = pathData.match(/[ML]\s*[\d.-]+\s*[\d.-]+/gi) || []; + + commands.forEach(function(cmd) { + var parts = cmd.match(/[\d.-]+/g); + if (parts && parts.length >= 2) { + points.push({ + x: parseFloat(parts[0]), + y: parseFloat(parts[1]) + }); + } + }); + + return points; + }, + + // Find nearest point on a line segment + nearestPointOnSegment: function(p1, p2, px, py) { + var dx = p2.x - p1.x; + var dy = p2.y - p1.y; + var lengthSq = dx * dx + dy * dy; + + if (lengthSq === 0) return {x: p1.x, y: p1.y}; + + var t = Math.max(0, Math.min(1, ((px - p1.x) * dx + (py - p1.y) * dy) / lengthSq)); + + return { + x: p1.x + t * dx, + y: p1.y + t * dy + }; + }, + + // Show visual marker at junction point + showJunctionMarker: function(point) { + var svgNS = 'http://www.w3.org/2000/svg'; + var existing = this.svgElement.querySelector('.junction-marker'); + if (existing) existing.remove(); + + var marker = document.createElementNS(svgNS, 'g'); + marker.setAttribute('class', 'junction-marker'); + + // Pulsing circle + var circle = document.createElementNS(svgNS, 'circle'); + circle.setAttribute('cx', point.x); + circle.setAttribute('cy', point.y); + circle.setAttribute('r', '8'); + circle.setAttribute('fill', '#f39c12'); + circle.setAttribute('stroke', '#fff'); + circle.setAttribute('stroke-width', '2'); + circle.style.animation = 'junctionPulse 1s infinite'; + marker.appendChild(circle); + + // Add pulse animation style if not exists (opacity + stroke only - NO scale to avoid position shift) + if (!document.getElementById('junction-pulse-style')) { + var style = document.createElement('style'); + style.id = 'junction-pulse-style'; + style.textContent = '@keyframes junctionPulse { 0%, 100% { opacity: 1; stroke-width: 2px; } 50% { opacity: 0.6; stroke-width: 4px; } }'; + document.head.appendChild(style); + } + + this.svgElement.appendChild(marker); + }, + showWireGrid: function() { - console.log('showWireGrid called, svgElement:', this.svgElement); + this.log('showWireGrid called'); if (!this.svgElement) { - console.error('No SVG element found!'); + this.log('No SVG element found!'); return; } @@ -9027,44 +10236,59 @@ } // Routing-Bereich ober- und unterhalb der Blöcke - this.carriers.forEach(function(carrier) { + // Sortiere Carriers nach Y-Position für Zwischen-Berechnung + var sortedCarriers = this.carriers.slice().sort(function(a, b) { + return (a._y || 0) - (b._y || 0); + }); + + sortedCarriers.forEach(function(carrier, carrierIdx) { if (typeof carrier._y === 'undefined') return; var railCenterY = carrier._y + self.RAIL_HEIGHT / 2; var blockTop = railCenterY - self.BLOCK_HEIGHT / 2; var blockBottom = railCenterY + self.BLOCK_HEIGHT / 2; - // Raster oberhalb (Sammelschienen-Bereich) - for (var offsetY = 20; offsetY < 150; offsetY += 15) { + // Raster oberhalb (Sammelschienen-Bereich) - erweitert für Outputs oben + for (var offsetY = 20; offsetY < self.OUTPUT_ZONE_HEIGHT + self.ROUTING_ZONE_HEIGHT; offsetY += 15) { var routeY = blockTop - offsetY; if (routeY > 20 && yPositions.indexOf(routeY) === -1) yPositions.push(routeY); } - // Raster unterhalb (Abgang-Bereich) - for (var offsetY2 = 20; offsetY2 < 150; offsetY2 += 15) { + + // Raster unterhalb (Abgang-Bereich) - erweitert für Outputs unten + for (var offsetY2 = 20; offsetY2 < self.OUTPUT_ZONE_HEIGHT + self.ROUTING_ZONE_HEIGHT; offsetY2 += 15) { var routeY2 = blockBottom + offsetY2; if (routeY2 < svgHeight - 20 && yPositions.indexOf(routeY2) === -1) yPositions.push(routeY2); } + + // Routing-Zone ZWISCHEN zwei aufeinanderfolgenden Carriern + if (carrierIdx < sortedCarriers.length - 1) { + var nextCarrier = sortedCarriers[carrierIdx + 1]; + if (typeof nextCarrier._y !== 'undefined') { + var nextBlockTop = nextCarrier._y + self.RAIL_HEIGHT / 2 - self.BLOCK_HEIGHT / 2; + var routingZoneStart = blockBottom + 10; + var routingZoneEnd = nextBlockTop - 10; + + // Fülle die Zone zwischen den Carriern mit Grid-Punkten + for (var zoneY = routingZoneStart; zoneY < routingZoneEnd; zoneY += 15) { + if (yPositions.indexOf(zoneY) === -1) yPositions.push(zoneY); + } + } + } }); // Sort positions xPositions.sort(function(a, b) { return a - b; }); yPositions.sort(function(a, b) { return a - b; }); - // Draw grid points at terminal-aligned positions + // PERFORMANCE: Only store snap points, don't create visual DOM elements + // This reduces from potentially 5000+ DOM elements to just a data array xPositions.forEach(function(x) { yPositions.forEach(function(y) { - var circle = document.createElementNS(svgNS, 'circle'); - circle.setAttribute('cx', x); - circle.setAttribute('cy', y); - circle.setAttribute('r', '1.5'); - circle.setAttribute('fill', '#888'); - circle.setAttribute('opacity', '0.4'); - gridLayer.appendChild(circle); self.wireGridPoints.push({x: x, y: y}); pointCount++; }); }); - console.log('Wire grid: ' + xPositions.length + ' x-positions, ' + yPositions.length + ' y-positions'); + self.log('Wire grid: ' + xPositions.length + ' x-positions, ' + yPositions.length + ' y-positions = ' + pointCount + ' snap points'); // Create magnetic cursor indicator (shows nearest grid point) var magnetCursor = document.createElementNS(svgNS, 'circle'); @@ -9077,9 +10301,14 @@ magnetCursor.setAttribute('pointer-events', 'none'); gridLayer.appendChild(magnetCursor); - // Append grid layer at the end of SVG (on top of everything) - this.svgElement.appendChild(gridLayer); - console.log('Wire grid created with ' + pointCount + ' points'); + // Insert grid layer BEFORE terminals layer so terminals remain clickable + var terminalsLayer = this.svgElement.querySelector('.schematic-terminals-layer'); + if (terminalsLayer) { + this.svgElement.insertBefore(gridLayer, terminalsLayer); + } else { + this.svgElement.appendChild(gridLayer); + } + self.log('Wire grid created with ' + pointCount + ' snap points (no DOM elements)'); // Setup magnetic snap behavior this.setupMagneticSnap(); @@ -9209,7 +10438,7 @@ $(this.svgElement).off('mousemove.magneticSnap'); this.magnetSnappedPos = null; this.wireGridPoints = null; - console.log('Wire grid hidden'); + this.log('Wire grid hidden'); }, updateWirePreview: function() { @@ -9237,7 +10466,7 @@ d += ' L ' + this.wireDrawPoints[i].x + ' ' + this.wireDrawPoints[i].y; } preview.setAttribute('d', d); - console.log('Wire preview updated:', d); + this.log('Wire preview updated:', d); }, updateWirePreviewCursor: function(x, y) { @@ -9292,22 +10521,14 @@ }, cancelWireDrawing: function() { - this.wireDrawSourceEq = null; - this.wireDrawSourceTerm = null; - this.wireDrawPoints = []; - - // Remove all preview elements using native DOM - var elements = this.svgElement.querySelectorAll('.wire-draw-preview, .wire-draw-cursor, .wire-draw-cursor-dot, .wire-draw-cursor-line'); - elements.forEach(function(el) { el.remove(); }); - - // Reset terminal highlight - $(this.svgElement).find('.schematic-terminal-circle').attr('stroke', '#fff').attr('stroke-width', '2').css('cursor', ''); - + this.cleanupWireDrawState(); this.showMessage('Zeichnung abgebrochen', 'info'); }, finishWireDrawing: function(targetEqId, targetTermId) { var self = this; + this.log('finishWireDrawing called - targetEqId:', targetEqId, 'targetTermId:', targetTermId); + this.log('State: wireExtendMode:', this.wireExtendMode, 'wireExtendConnId:', this.wireExtendConnId); // Get target terminal position var eq = this.equipment.find(function(e) { return String(e.id) === String(targetEqId); }); @@ -9315,51 +10536,249 @@ var terminals = this.getTerminals(eq); var termPos = this.getTerminalPosition(eq, targetTermId, terminals); if (termPos) { - // Add orthogonal bend if needed before target - if (this.wireDrawPoints.length > 0) { - var lastPt = this.wireDrawPoints[this.wireDrawPoints.length - 1]; - // If not aligned, add intermediate point for 90° bend - if (lastPt.x !== termPos.x && lastPt.y !== termPos.y) { - // Go vertical first, then horizontal - this.wireDrawPoints.push({x: lastPt.x, y: termPos.y}); - } - } this.wireDrawPoints.push({x: termPos.x, y: termPos.y}); } } - // Build orthogonal path - ensure all segments are horizontal or vertical - var pathData = ''; - for (var i = 0; i < this.wireDrawPoints.length; i++) { - var pt = this.wireDrawPoints[i]; - if (i === 0) { - pathData += 'M ' + pt.x + ' ' + pt.y + ' '; - } else { - var prevPt = this.wireDrawPoints[i - 1]; - // If diagonal, add intermediate point - if (prevPt.x !== pt.x && prevPt.y !== pt.y) { - pathData += 'L ' + prevPt.x + ' ' + pt.y + ' '; - } - pathData += 'L ' + pt.x + ' ' + pt.y + ' '; + // Handle EXTEND mode - update existing connection + if (this.wireExtendMode && this.wireExtendConnId) { + // Combine existing points with new points (skip first point of new since it's the join point) + var allPoints = this.wireExtendExistingPoints.concat(this.wireDrawPoints.slice(1)); + + // Build combined path + var pathData = ''; + for (var i = 0; i < allPoints.length; i++) { + pathData += (i === 0 ? 'M' : 'L') + ' ' + allPoints[i].x + ' ' + allPoints[i].y + ' '; } + + // Update the existing connection with new path and target + this.updateExtendedConnection(this.wireExtendConnId, targetEqId, targetTermId, pathData.trim()); + // Extend mode: don't keep draw mode + this.cleanupWireDrawState(false); + return; } - // Show connection dialog to set labels and save - this.showConnectionLabelDialogWithPath( - this.wireDrawSourceEq, - this.wireDrawSourceTerm, - targetEqId, - targetTermId, - pathData.trim() - ); + // Build path string from points + var pathData = ''; + for (var i = 0; i < this.wireDrawPoints.length; i++) { + pathData += (i === 0 ? 'M' : 'L') + ' ' + this.wireDrawPoints[i].x + ' ' + this.wireDrawPoints[i].y + ' '; + } - // Cleanup + // Handle junction source vs terminal source + if (this.wireDrawFromJunction) { + // Junction source - inherit from source connection + this._inheritFromConnection = this.wireDrawJunctionConn; + this._pendingPathData = pathData.trim(); + // Junction connections have no source equipment (fk_source = NULL) + this.showConnectionLabelDialog(null, null, targetEqId, targetTermId); + } else { + // Normal terminal source + this._pendingPathData = pathData.trim(); + this.showConnectionLabelDialog( + this.wireDrawSourceEq, + this.wireDrawSourceTerm, + targetEqId, + targetTermId + ); + } + + // Cleanup - keep draw mode active for drawing multiple connections + this.cleanupWireDrawState(true); + }, + + cleanupWireDrawState: function(keepDrawMode) { this.wireDrawSourceEq = null; this.wireDrawSourceTerm = null; + this.wireDrawFromJunction = false; + this.wireDrawJunctionConn = null; + this.wireDrawJunctionPoint = null; this.wireDrawPoints = []; - var elements = this.svgElement.querySelectorAll('.wire-draw-preview, .wire-draw-cursor, .wire-draw-cursor-dot, .wire-draw-cursor-line'); + // Extend mode cleanup + this.wireExtendMode = false; + this.wireExtendConnId = null; + this.wireExtendExistingPoints = []; + + // Remove preview elements + var elements = this.svgElement.querySelectorAll('.wire-draw-preview, .wire-draw-cursor, .wire-draw-cursor-dot, .wire-draw-cursor-line, .junction-marker, .extend-start-marker'); elements.forEach(function(el) { el.remove(); }); - $(this.svgElement).find('.schematic-terminal-circle').attr('stroke', '#fff').attr('stroke-width', '2').css('cursor', ''); + + // Reset wire draw mode only if not keeping it + if (!keepDrawMode) { + this.wireDrawMode = false; + var $btn = $('.schematic-wire-draw-toggle'); + $btn.removeClass('active').css('background', ''); + $(this.svgElement).find('.schematic-terminal-circle').attr('stroke', '#fff').attr('stroke-width', '2'); + $(this.svgElement).find('.schematic-terminal').css('cursor', ''); + $(this.svgElement).find('.schematic-connection-hitarea').css('cursor', ''); + } else { + // Keep draw mode active - just reset terminal highlights + $(this.svgElement).find('.schematic-terminal-circle').attr('stroke', '#fff').attr('stroke-width', '2'); + this.showMessage('Zeichenmodus aktiv - nächste Verbindung zeichnen', 'info'); + } + }, + + // Update an existing connection with extended path + updateExtendedConnection: function(connId, newTargetEqId, newTargetTermId, newPathData) { + var self = this; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'update', + connection_id: connId, + fk_target: newTargetEqId, + target_terminal_id: newTargetTermId, + path_data: newPathData, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Leitung verlängert', 'success'); + self.loadConnections(); + } else { + self.showMessage('Fehler: ' + (response.error || 'Unbekannt'), 'error'); + } + }, + error: function() { + self.showMessage('Netzwerkfehler', 'error'); + } + }); + }, + + // Dialog for creating a connection from a junction point + showJunctionConnectionDialog: function(sourceConnId, junctionPoint, targetEqId, targetTermId, pathData) { + var self = this; + + // Get source connection info for context + var sourceConn = this.connections.find(function(c) { return String(c.id) === String(sourceConnId); }); + var targetEq = this.equipment.find(function(e) { return String(e.id) === String(targetEqId); }); + + var sourceLabel = sourceConn ? (sourceConn.output_label || sourceConn.connection_type || 'Leitung') : 'Leitung'; + + var html = '
'; + html += '
'; + html += '

Abzweigung erstellen

'; + html += '×
'; + html += '
'; + + html += '
'; + html += ' Abzweig von: ' + this.escapeHtml(sourceLabel) + ''; + html += ' '; + html += '' + this.escapeHtml((targetEq ? targetEq.label || targetEq.type_label_short : 'Gerät')) + ''; + html += '
'; + + html += '
'; + html += ''; + html += ''; + html += '
'; + + html += '
'; + html += ''; + html += '
'; + + var typePresets = ['L1', 'L2', 'L3', 'N', 'PE', 'L1N', '3P']; + // Pre-select the same type as source connection + var defaultType = sourceConn ? sourceConn.connection_type : 'L1N'; + + typePresets.forEach(function(t) { + var color = self.PHASE_COLORS[t] || self.COLORS.connection; + var selected = (t === defaultType) ? 'outline:2px solid #fff;' : ''; + html += ''; + }); + + html += '
'; + html += ''; + html += ''; + html += '
'; + + html += '
'; + html += '
'; + + $('body').append(html); + + // Event handlers + $('.kundenkarte-modal-close, #conn-cancel').on('click', function() { + $('#schematic-conn-dialog').remove(); + }); + + $('.conn-type-btn').on('click', function() { + $('.conn-type-btn').css('outline', 'none'); + $(this).css('outline', '2px solid #fff'); + $('#conn-type').val($(this).data('type')); + $('#conn-color').val($(this).data('color')); + }); + + $('#conn-save').on('click', function() { + var label = $('#conn-label').val().trim(); + var connType = $('#conn-type').val(); + var color = $('#conn-color').val(); + + // Create connection via AJAX - junction connections have no fk_source + // but we store junction info in path_data and source_connection_id + self.saveJunctionConnection({ + anlage_id: self.anlageId, + fk_source: null, // No source equipment + source_terminal_id: null, + fk_target: targetEqId, + target_terminal_id: targetTermId, + output_label: label, + connection_type: connType, + color: color, + path_data: pathData, + junction_connection_id: sourceConnId, + junction_x: junctionPoint.x, + junction_y: junctionPoint.y + }); + + $('#schematic-conn-dialog').remove(); + }); + }, + + saveJunctionConnection: function(data) { + var self = this; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'create', + fk_source: data.fk_source, + source_terminal_id: data.source_terminal_id, + fk_target: data.fk_target, + target_terminal_id: data.target_terminal_id, + connection_type: data.connection_type || 'L1N', + color: data.color || self.COLORS.connection, + output_label: data.output_label || '', + medium_type: data.medium_type || '', + medium_length: data.medium_length || '', + path_data: data.path_data || '', + bundled_terminals: data.bundled_terminals || '', + // Junction-specific fields + junction_connection_id: data.junction_connection_id || '', + junction_x: data.junction_x || '', + junction_y: data.junction_y || '', + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Abzweigung erstellt', 'success'); + self.loadConnections(); + } else { + self.showMessage('Fehler: ' + (response.error || 'Unbekannt'), 'error'); + } + }, + error: function() { + self.showMessage('Verbindungsfehler', 'error'); + } + }); }, showConnectionLabelDialogWithPath: function(sourceEqId, sourceTermId, targetEqId, targetTermId, pathData) { @@ -9375,6 +10794,30 @@ var sourceEq = this.equipment.find(function(e) { return e.id == sourceEqId; }); var targetEq = this.equipment.find(function(e) { return e.id == targetEqId; }); + // Check if we should inherit from a specific connection (clicked wire) + var existingConn = this._inheritFromConnection || null; + this._inheritFromConnection = null; // Clear after use + + // If no specific connection, look for existing connections to target + if (!existingConn && targetEqId && this.connections && this.connections.length > 0) { + // Look for connections where target is this equipment (incoming) + existingConn = this.connections.find(function(c) { + return c.fk_target == targetEqId; + }); + // If not found, look for connections from target (outgoing) + if (!existingConn) { + existingConn = this.connections.find(function(c) { + return c.fk_source == targetEqId; + }); + } + } + + // Default values or inherited from existing connection + var defaultType = existingConn && existingConn.connection_type ? existingConn.connection_type : 'L1N'; + var defaultColor = existingConn && existingConn.color ? existingConn.color : this.COLORS.connection; + var defaultMedium = existingConn && existingConn.medium_type ? existingConn.medium_type : ''; + var defaultLength = existingConn && existingConn.medium_length ? existingConn.medium_length : ''; + var html = '
'; html += '
'; html += '

Verbindung erstellen

'; @@ -9399,24 +10842,37 @@ var typePresets = ['L1', 'L2', 'L3', 'N', 'PE', 'L1N', '3P']; typePresets.forEach(function(t) { var color = self.PHASE_COLORS[t] || self.COLORS.connection; + var isSelected = (t === defaultType); + var outlineStyle = isSelected ? 'outline:2px solid #fff;' : ''; html += ''; }); html += '
'; - html += ''; - html += ''; + html += ''; + html += ''; html += '
'; html += '
'; html += ''; html += '
'; - html += ''; - html += ''; + html += ''; + html += ''; html += '
'; html += '
'; + // Bundled terminals checkbox - only show if source equipment has >1 terminal + var sourceTerminalCount = sourceEq ? this.getTerminalCount(sourceEq) : 1; + if (sourceTerminalCount > 1) { + html += '
'; + html += ''; + html += '
'; + } + html += '
'; html += '