diff --git a/js/kundenkarte.js b/js/kundenkarte.js index b7faf68..d0202ac 100644 --- a/js/kundenkarte.js +++ b/js/kundenkarte.js @@ -5314,6 +5314,11 @@ var eqId = String(equipmentId); var termId = String(terminalId); + // Zuerst Terminal-Color-Map prüfen (vom Eingangs-Anschlusspunkt propagierte Farbe) + if (this._terminalColorMap && this._terminalColorMap[eqId] && this._terminalColorMap[eqId][termId]) { + return this._terminalColorMap[eqId][termId]; + } + if (!this.connections || this.connections.length === 0) { return null; } @@ -5487,6 +5492,222 @@ }); }, + // Terminal-Phasen-Map: Ordnet jedem Terminal die richtige Phase und Farbe zu + // Propagation: Anschlusspunkte → Leitungen → Block-Durchreichung → Busbar-Verteilung + // WICHTIG: Nur Terminals die tatsächlich über Verbindungen erreichbar sind bekommen Farben + // Die Farbe wird vom Eingangs-Anschlusspunkt bestimmt (conn.color), nicht vom Phasennamen + buildTerminalPhaseMap: function() { + var self = this; + this._terminalPhaseMap = {}; // {eqId: {termId: "L1"}} - Phase-Name für Busbar-Logik + this._terminalColorMap = {}; // {eqId: {termId: "#hex"}} - Tatsächliche Farbe für Rendering + + // Hilfsfunktion: Phase + Farbe setzen, gibt true zurück wenn neu gesetzt + var setPhase = function(eqId, termId, phase, color) { + if (!self._terminalPhaseMap[eqId]) self._terminalPhaseMap[eqId] = {}; + if (!self._terminalColorMap[eqId]) self._terminalColorMap[eqId] = {}; + if (self._terminalPhaseMap[eqId][termId]) return false; // Schon gesetzt + self._terminalPhaseMap[eqId][termId] = phase; + self._terminalColorMap[eqId][termId] = color || self.getWireColor(phase); + return true; + }; + + // Hilfsfunktion: Phase + Farbe überschreiben (für Busbar-Verteilung) + var forcePhase = function(eqId, termId, phase, color) { + if (!self._terminalPhaseMap[eqId]) self._terminalPhaseMap[eqId] = {}; + if (!self._terminalColorMap[eqId]) self._terminalColorMap[eqId] = {}; + if (self._terminalPhaseMap[eqId][termId] === phase) return false; + self._terminalPhaseMap[eqId][termId] = phase; + self._terminalColorMap[eqId][termId] = color || self.getWireColor(phase); + return true; + }; + + // Farbe für ein Terminal holen + var getColor = function(eqId, termId) { + return (self._terminalColorMap[eqId] || {})[termId] || null; + }; + + // --- Schritt 1: Anschlusspunkte (Inputs) als Startpunkte --- + // Connections ohne fk_source = Einspeisungen + // Die Farbe vom Eingang (conn.color) bestimmt alle verbundenen Terminals + this.connections.forEach(function(conn) { + if (parseInt(conn.is_rail) === 1) return; + if (conn.fk_source) return; // Hat Source → kein Anschlusspunkt + if (!conn.fk_target || !conn.target_terminal_id) return; + + var phase = (conn.connection_type || '').toUpperCase(); + if (!self.PHASE_COLORS[phase]) return; // Nur echte Phasen (L1, L2, L3, N, PE) + + // Eingangsfarbe: conn.color hat Vorrang, sonst Standard-Phasenfarbe + var inputColor = conn.color || self.getWireColor(phase); + setPhase(conn.fk_target, conn.target_terminal_id, phase, inputColor); + }); + + // --- Iterativ propagieren bis keine Änderungen mehr --- + var changed = true; + var iterations = 0; + var maxIterations = 20; + + while (changed && iterations++ < maxIterations) { + changed = false; + + // Block-Durchreichung (top ↔ bottom) - Farbe wird mitpropagiert + self.equipment.forEach(function(eq) { + var terminals = self.getTerminals(eq); + var topTerminals = terminals.filter(function(t) { return t.pos === 'top'; }); + var bottomTerminals = terminals.filter(function(t) { return t.pos === 'bottom'; }); + + var pairCount = Math.min(topTerminals.length, bottomTerminals.length); + for (var i = 0; i < pairCount; i++) { + var topPhase = (self._terminalPhaseMap[eq.id] || {})[topTerminals[i].id]; + var botPhase = (self._terminalPhaseMap[eq.id] || {})[bottomTerminals[i].id]; + + if (topPhase && !botPhase) { + var topColor = getColor(eq.id, topTerminals[i].id); + if (setPhase(eq.id, bottomTerminals[i].id, topPhase, topColor)) changed = true; + } else if (botPhase && !topPhase) { + var botColor = getColor(eq.id, bottomTerminals[i].id); + if (setPhase(eq.id, topTerminals[i].id, botPhase, botColor)) changed = true; + } + } + }); + + // Leitungen propagieren (Phase + Farbe von einem Ende zum anderen) + self.connections.forEach(function(conn) { + if (parseInt(conn.is_rail) === 1) return; + if (!conn.fk_source || !conn.fk_target) return; + if (!conn.source_terminal_id || !conn.target_terminal_id) return; + + var srcPhase = (self._terminalPhaseMap[conn.fk_source] || {})[conn.source_terminal_id]; + var tgtPhase = (self._terminalPhaseMap[conn.fk_target] || {})[conn.target_terminal_id]; + + if (srcPhase && !tgtPhase) { + var srcColor = getColor(conn.fk_source, conn.source_terminal_id); + if (setPhase(conn.fk_target, conn.target_terminal_id, srcPhase, srcColor)) changed = true; + } else if (tgtPhase && !srcPhase) { + var tgtColor = getColor(conn.fk_target, conn.target_terminal_id); + if (setPhase(conn.fk_source, conn.source_terminal_id, tgtPhase, tgtColor)) changed = true; + } + }); + + // Busbar-Verteilung: Nur Phasen verteilen die tatsächlich eingespeist sind + self.connections.forEach(function(busbar) { + if (parseInt(busbar.is_rail) !== 1) return; + + var carrier = self.getCarrierById(busbar.fk_carrier); + if (!carrier) return; + + var railStart = parseInt(busbar.rail_start_te) || 1; + var railEnd = parseInt(busbar.rail_end_te) || railStart; + var posY = parseInt(busbar.position_y) || 0; + var targetPos = (posY === 0) ? 'top' : 'bottom'; + + // Phase-Labels ermitteln + var phaseLabels; + if (busbar.phases_config && Array.isArray(busbar.phases_config) && busbar.phases_config.length > 0) { + phaseLabels = busbar.phases_config; + } else { + var phases = busbar.rail_phases || busbar.connection_type || ''; + phaseLabels = self.parsePhaseLabels(phases); + } + if (phaseLabels.length === 0) return; + + // Sammle welche Phasen tatsächlich eingespeist sind + deren Farbe + var fedPhases = {}; + var fedColors = {}; // Phase → Farbe vom Eingangs-Anschlusspunkt + self.equipment.forEach(function(eq) { + var eqCarrierId = eq.carrier_id || eq.fk_carrier; + if (String(eqCarrierId) !== String(busbar.fk_carrier)) return; + var eqPosTE = parseFloat(eq.position_te) || 1; + var eqWidthTE = parseFloat(eq.width_te) || 1; + if (!(eqPosTE < railEnd + 1 && railStart < eqPosTE + eqWidthTE)) return; + + var terminals = self.getTerminals(eq); + terminals.filter(function(t) { return t.pos === targetPos; }).forEach(function(term) { + var phase = (self._terminalPhaseMap[eq.id] || {})[term.id]; + if (phase) { + fedPhases[phase] = true; + // Farbe vom einspeisenden Terminal übernehmen + if (!fedColors[phase]) { + fedColors[phase] = getColor(eq.id, term.id); + } + } + }); + }); + + // Keine eingespeisten Phasen → Busbar inaktiv + if (Object.keys(fedPhases).length === 0) return; + + // Excluded TEs + var excludedTEs = []; + if (busbar.excluded_te) { + excludedTEs = busbar.excluded_te.split(',').map(function(t) { + return parseInt(t.trim()); + }).filter(function(t) { return !isNaN(t); }); + } + + // Busbar-Muster auf Equipment-Terminals verteilen, + // aber NUR für Phasen die tatsächlich eingespeist sind + // Die Farbe kommt vom Eingangs-Anschlusspunkt (fedColors) + self.equipment.forEach(function(eq) { + var eqCarrierId = eq.carrier_id || eq.fk_carrier; + if (String(eqCarrierId) !== String(busbar.fk_carrier)) return; + var eqPosTE = parseFloat(eq.position_te) || 1; + var eqWidthTE = parseFloat(eq.width_te) || 1; + if (!(eqPosTE < railEnd + 1 && railStart < eqPosTE + eqWidthTE)) return; + + var terminals = self.getTerminals(eq); + var posTerminals = terminals.filter(function(t) { return t.pos === targetPos; }); + + posTerminals.forEach(function(term, idx) { + var teIndex = term.col !== undefined ? term.col : (idx % eqWidthTE); + var absoluteTE = Math.round(eqPosTE + teIndex); + + if (excludedTEs.indexOf(absoluteTE) !== -1) return; + if (absoluteTE < railStart || absoluteTE > railEnd) return; + + var teOffset = absoluteTE - railStart; + var phase = phaseLabels[teOffset % phaseLabels.length]; + + // Nur verteilen wenn diese Phase eingespeist ist + if (!fedPhases[phase]) return; + + // Farbe vom Eingang, Fallback auf Standard-Phasenfarbe + var phaseColor = fedColors[phase] || self.getWireColor(phase); + if (forcePhase(eq.id, term.id, phase, phaseColor)) changed = true; + }); + }); + }); + } + + // --- Leitungsfarben-Map: Leitungen die mit Eingängen verbunden sind --- + // Farbe wird von der Terminal-Color-Map genommen (= Eingangsfarbe) + this._connectionColorMap = {}; + this.connections.forEach(function(conn) { + if (parseInt(conn.is_rail) === 1) return; + // Anschlusspunkte (kein fk_source, kein path_data) überspringen + if (!conn.fk_source && !conn.path_data) return; + // Abgänge (fk_source, kein fk_target, kein path_data) behalten ihre Farbe + if (conn.fk_source && !conn.fk_target && !conn.path_data) return; + + // Farbe vom Source- oder Target-Terminal nehmen (aus _terminalColorMap) + var color = null; + if (conn.fk_source && conn.source_terminal_id) { + color = getColor(conn.fk_source, conn.source_terminal_id); + } + if (!color && conn.fk_target && conn.target_terminal_id) { + color = getColor(conn.fk_target, conn.target_terminal_id); + } + + if (color) { + self._connectionColorMap[conn.id] = color; + } + }); + + this.log('buildTerminalPhaseMap: ' + Object.keys(this._terminalPhaseMap).length + + ' Equipment, ' + Object.keys(this._connectionColorMap).length + + ' Leitungen mit Phasen-Zuordnung (' + iterations + ' Iterationen)'); + }, + // O(1) lookup helpers getEquipmentById: function(id) { return id ? this._equipmentById[id] : null; @@ -5971,6 +6192,8 @@ // SVG click for wire drawing - add waypoint $(document).off('click.wireDrawSvg').on('click.wireDrawSvg', '.schematic-editor-canvas svg', function(e) { if (!self.wireDrawMode) return; + // Nach Wire-Drag keinen Waypoint setzen + if (self._wireDragJustEnded) return; // Only add waypoints after source terminal is selected if (!self.wireDrawSourceEq) { // Show hint if clicking on canvas without selecting terminal first @@ -6690,6 +6913,9 @@ // Build index maps for O(1) lookups (performance optimization) self.buildIndexes(); + // Terminal-Phasen-Map aufbauen (Busbar → Equipment → Durchreichung) + self.buildTerminalPhaseMap(); + // Initialize canvas now that all data is loaded if (!self.isInitialized) { self.initCanvas(); @@ -6847,9 +7073,9 @@ // Layers (order matters: back to front) // Connections BEHIND blocks so wires "go through" blocks visually svg += ''; - svg += ''; // Wires behind blocks + svg += ''; // Leitungen hinter Blöcken + svg += ''; // Phasenschienen hinter Blöcken (Tap-Lines unter Blöcken) svg += ''; - svg += ''; // Distribution busbars (Phasenschienen) svg += ''; // Connection preview line @@ -6879,6 +7105,7 @@ $svg.attr('height', this.layoutHeight); this.renderRails(); + this.buildTerminalPhaseMap(); this.renderBlocks(); this.renderBridges(); this.renderBusbars(); @@ -7201,13 +7428,11 @@ var widthTE = parseFloat(eq.width_te) || 1; - // Check if this equipment is covered by a busbar (returns { top: bool, bottom: bool }) - var busbarCoverage = self.isEquipmentCoveredByBusbar(eq); - + // Busbar-Abdeckung prüfen (Terminals unter Phasenschiene verstecken) // Terminal-Position aus Equipment-Typ (both, top_only, bottom_only) var terminalPos = eq.type_terminal_position || 'both'; - var showTopTerminals = (terminalPos === 'both' || terminalPos === 'top_only') && !busbarCoverage.top; - var showBottomTerminals = (terminalPos === 'both' || terminalPos === 'bottom_only') && !busbarCoverage.bottom; + var showTopTerminals = (terminalPos === 'both' || terminalPos === 'top_only'); + var showBottomTerminals = (terminalPos === 'both' || terminalPos === 'bottom_only'); // Top terminals - im TE-Raster platziert (hide if busbar covers this equipment or terminal_position) // Now supports stacked terminals with 'row' property (for Reihenklemmen) @@ -7464,8 +7689,10 @@ busbarY = blockBottom + 25 + (busbarIndex * busbarSpacing); } - // Color from connection or default phase color - var color = conn.color || self.PHASE_COLORS[conn.connection_type] || '#e74c3c'; + // Farbe(n) aus Connection - kann komma-getrennt sein bei Mehrphasen-Busbars + var colorRaw = conn.color || self.PHASE_COLORS[conn.connection_type] || '#e74c3c'; + var colors = colorRaw.split(',').map(function(c) { return c.trim(); }); + var primaryColor = colors[0]; // Erste Farbe für den Balken // Draw busbar as rounded rectangle html += ''; @@ -7478,7 +7705,7 @@ // Main bar html += ''; + html += 'rx="2" fill="#666" stroke="#222" stroke-width="1" style="cursor:pointer;"/>'; // Parse excluded TEs var excludedTEs = []; @@ -7523,11 +7750,7 @@ html += ''; - - // Connector dot at equipment end - same size as block terminals - html += ''; + html += 'stroke="#666" stroke-width="2" stroke-linecap="round"/>'; // Phase label at this TE connection if (currentPhase) { @@ -7546,7 +7769,7 @@ var badgeX = endX + 5; var badgeY = busbarY + (busbarHeight - 16) / 2; html += ''; + html += 'fill="#2d2d44" stroke="#666" stroke-width="1"/>'; html += ' Phase-Farbe > Fallback hellblau + var inputColor = conn.color || self.PHASE_COLORS[conn.connection_type] || '#4fc3f7'; // Calculate line length based on label var inputLabel = conn.output_label || ''; @@ -7814,13 +8039,20 @@ html += conn.connection_type || 'L1'; html += ''; - // Optional label on side (vertical) + // Bezeichnung als Badge neben der Eingangsleitung if (conn.output_label) { - var labelY = targetPos.y - inputLineLength / 2; - html += ''; - html += self.escapeHtml(conn.output_label); + var badgeText = self.escapeHtml(conn.output_label); + var badgeWidth = Math.min(badgeText.length * 7 + 16, 140); + var badgeHeight = 20; + var badgeX = targetPos.x + 14; + var badgeY = startY + (inputLineLength / 2) - badgeHeight / 2; + + html += ''; + html += ' 1) { + 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 = this.getTerminalPosition(sourceEq, t.id, sourceTerminals); + return pos ? pos.x : lineX; + }.bind(this)); + lineX = (Math.min.apply(null, positions) + Math.max.apply(null, positions)) / 2; + } + } + + var labelText = conn.output_label || ''; + var cableText = (conn.medium_type || '') + ' ' + (conn.medium_spec || ''); + var maxTextLen = Math.max(labelText.length, cableText.trim().length); + var lineLength = Math.min(120, Math.max(50, maxTextLen * 6 + 20)); + var goingUp = sourcePos.isTop; + var endY = goingUp ? (sourcePos.y - lineLength) : (sourcePos.y + lineLength); + pathData = 'M ' + lineX + ' ' + sourcePos.y + ' L ' + lineX + ' ' + endY; + } else if (!conn.fk_source && targetEq) { + // Anschlusspunkt (Input) - Linie von oben ins Terminal + var targetTerminals = this.getTerminals(targetEq); + var targetTermId = conn.target_terminal_id || 't1'; + var targetPos = this.getTerminalPosition(targetEq, targetTermId, targetTerminals); + if (!targetPos) return null; + + var inputLabel = conn.output_label || ''; + var inputLineLength = Math.min(80, Math.max(45, inputLabel.length * 5 + 30)); + var startY = targetPos.y - inputLineLength; + pathData = 'M ' + targetPos.x + ' ' + startY + ' L ' + targetPos.x + ' ' + targetPos.y; + } + + return pathData; + }, + + /** + * Einen 2-Punkt-Pfad in einen 4-Punkt-Pfad umwandeln, + * damit die mittleren Segmente gedraggt werden können. + * Aus einer Linie A→B wird: A → Knick1 → Knick2 → B + */ + ensureDraggablePath: function(points) { + if (points.length >= 4) return points; + + if (points.length === 2) { + var p1 = points[0]; + var p2 = points[1]; + var isVertical = Math.abs(p1.x - p2.x) < 2; + var isHorizontal = Math.abs(p1.y - p2.y) < 2; + + if (isVertical) { + // Vertikale Linie → Z-Form mit horizontalem Mittelsegment + var midY1 = p1.y + (p2.y - p1.y) * 0.33; + var midY2 = p1.y + (p2.y - p1.y) * 0.66; + return [ + { x: p1.x, y: p1.y }, + { x: p1.x, y: midY1 }, + { x: p1.x, y: midY2 }, + { x: p2.x, y: p2.y } + ]; + } else if (isHorizontal) { + // Horizontale Linie → Z-Form mit vertikalem Mittelsegment + var midX1 = p1.x + (p2.x - p1.x) * 0.33; + var midX2 = p1.x + (p2.x - p1.x) * 0.66; + return [ + { x: p1.x, y: p1.y }, + { x: midX1, y: p1.y }, + { x: midX2, y: p2.y }, + { x: p2.x, y: p2.y } + ]; + } else { + // Diagonale Linie → Z-Form mit Knick in der Mitte + var midY = (p1.y + p2.y) / 2; + return [ + { x: p1.x, y: p1.y }, + { x: p1.x, y: midY }, + { x: p2.x, y: midY }, + { x: p2.x, y: p2.y } + ]; + } + } + + if (points.length === 3) { + // 3 Punkte = 2 Segmente, nur 1 mittleres Segment aber das ist index 0 oder 1 + // Problem: index 0 und length-2 (=1) sind gesperrt + // Lösung: Zwischenpunkt in das längere Segment einfügen + var seg0Len = Math.abs(points[1].x - points[0].x) + Math.abs(points[1].y - points[0].y); + var seg1Len = Math.abs(points[2].x - points[1].x) + Math.abs(points[2].y - points[1].y); + + if (seg0Len >= seg1Len) { + // Erstes Segment teilen + var mid = { + x: (points[0].x + points[1].x) / 2, + y: (points[0].y + points[1].y) / 2 + }; + return [points[0], mid, points[1], points[2]]; + } else { + // Zweites Segment teilen + var mid = { + x: (points[1].x + points[2].x) / 2, + y: (points[1].y + points[2].y) / 2 + }; + return [points[0], points[1], mid, points[2]]; + } + } + + return points; + }, + + /** + * Start dragging a wire segment. + * Shift+Linksklick oder Mittelklick auf einem Leitungssegment. + * Horizontale Segmente werden vertikal verschoben, + * vertikale Segmente werden horizontal verschoben. + * Start- und End-Segmente (an Terminals) bleiben fixiert. */ startWireDrag: function(connId, clickX, clickY) { var conn = this.connections.find(function(c) { return c.id == connId; }); - if (!conn || !conn.path_data) return false; + if (!conn) { + console.warn('[WireDrag] Connection nicht gefunden:', connId); + return false; + } - var points = this.parsePathToPoints(conn.path_data); - if (points.length < 3) return false; // Need at least 3 points to have a draggable segment + console.log('[WireDrag] Start für Connection #' + connId, 'click:', clickX, clickY, 'fk_source:', conn.fk_source, 'fk_target:', conn.fk_target); + + // Pfad-Daten ermitteln - bevorzugt aus dem tatsächlich gerenderten SVG lesen + var pathData = conn.path_data; + if (!pathData) { + // Zuerst den gerenderten SVG-Pfad aus dem DOM lesen (stimmt mit Anzeige überein) + var $svgPath = $(this.svgElement).find('.schematic-connection[data-connection-id="' + connId + '"]'); + if ($svgPath.length > 0) { + pathData = $svgPath.attr('d'); + console.log('[WireDrag] Pfad aus DOM gelesen:', pathData); + } + // Fallback: Pfad generieren + if (!pathData) { + pathData = this.generatePathDataForDrag(conn); + console.log('[WireDrag] Pfad generiert:', pathData); + } + if (!pathData) { + console.warn('[WireDrag] Pfad konnte nicht generiert werden'); + this.showMessage('Pfad konnte nicht generiert werden', 'warning'); + return false; + } + conn.path_data = pathData; + } + + var points = this.parsePathToPoints(pathData); + console.log('[WireDrag] Punkte geparst:', points.length, JSON.stringify(points)); + + // Sicherstellen dass genug Punkte für draggbare Segmente vorhanden sind + points = this.ensureDraggablePath(points); + console.log('[WireDrag] Nach ensureDraggablePath:', points.length, JSON.stringify(points)); + + if (points.length < 4) { + console.warn('[WireDrag] Zu wenige Punkte:', points.length); + return false; + } var segment = this.findClickedSegment(points, clickX, clickY); - if (!segment) return false; + if (!segment) { + console.warn('[WireDrag] Kein Segment gefunden bei', clickX, clickY); + return false; + } - // Don't allow dragging the first or last segment (connected to terminals) - if (segment.index === 0 || segment.index === points.length - 2) { - this.showMessage('Start- und End-Segmente können nicht verschoben werden', 'warning'); + console.log('[WireDrag] Segment gefunden:', segment.index, '/', (points.length - 2), 'horizontal:', segment.isHorizontal); + + // Segmente an Terminals fixiert → nur sperren wenn tatsächlich ein Terminal da ist + var isFirstSegment = (segment.index === 0); + var isLastSegment = (segment.index === points.length - 2); + var hasSource = conn.fk_source && conn.fk_source != '0'; + var hasTarget = conn.fk_target && conn.fk_target != '0'; + + if (isFirstSegment && hasSource && isLastSegment && hasTarget) { + console.warn('[WireDrag] Einziges Segment, beide Enden fixiert'); + this.showMessage('Segment kann nicht verschoben werden', 'warning'); + return false; + } + if (isFirstSegment && hasSource) { + console.warn('[WireDrag] Erstes Segment, am Source-Terminal fixiert'); + this.showMessage('Start-Segment ist am Terminal fixiert', 'warning'); + return false; + } + if (isLastSegment && hasTarget) { + console.warn('[WireDrag] Letztes Segment, am Target-Terminal fixiert'); + this.showMessage('End-Segment ist am Terminal fixiert', 'warning'); return false; } @@ -10974,15 +11480,17 @@ this.wireDragStartPos = { x: clickX, y: clickY }; this.wireDragOriginalPoints = JSON.parse(JSON.stringify(points)); - // Visual feedback + // Visuelles Feedback - Cursor zeigt Verschiebungsrichtung $(this.svgElement).css('cursor', segment.isHorizontal ? 'ns-resize' : 'ew-resize'); - this.log('Wire drag started - segment:', segment.index, 'horizontal:', segment.isHorizontal); + this.log('Wire drag gestartet - Segment:', segment.index, 'horizontal:', segment.isHorizontal); return true; }, /** - * Handle mouse move during wire drag + * Handle mouse move during wire drag. + * Verschiebt das angeklickte Segment und aktualisiert den SVG-Pfad live. + * Die angrenzenden Segmente strecken/kürzen sich automatisch mit. */ handleWireDragMove: function(mouseX, mouseY) { if (!this.wireDragMode) return; @@ -10991,42 +11499,273 @@ var segIdx = this.wireDragSegmentIndex; if (this.wireDragIsHorizontal) { - // Horizontal segment - move Y (vertical drag) + // Horizontales Segment vertikal verschieben var deltaY = mouseY - this.wireDragStartPos.y; var newY = points[segIdx].y + deltaY; - - // Snap to grid newY = Math.round(newY / this.WIRE_GRID_SIZE) * this.WIRE_GRID_SIZE; - - // Update both points of this segment points[segIdx].y = newY; points[segIdx + 1].y = newY; } else { - // Vertical segment - move X (horizontal drag) + // Vertikales Segment horizontal verschieben var deltaX = mouseX - this.wireDragStartPos.x; var newX = points[segIdx].x + deltaX; - - // Snap to grid newX = Math.round(newX / this.WIRE_GRID_SIZE) * this.WIRE_GRID_SIZE; - - // Update both points of this segment points[segIdx].x = newX; points[segIdx + 1].x = newX; } - // Update the visual path in SVG + // SVG-Pfad aktualisieren (alle Elemente mit dieser Connection-ID) var newPath = this.pointsToPath(points); - var $conn = $(this.svgElement).find('.schematic-connection[data-connection-id="' + this.wireDragConnId + '"]'); - var $hitarea = $(this.svgElement).find('.schematic-connection-hitarea[data-connection-id="' + this.wireDragConnId + '"]'); - var $shadow = $conn.closest('.schematic-connection-group').find('.schematic-connection-shadow'); + var connIdStr = this.wireDragConnId; + var $svg = $(this.svgElement); - $conn.attr('d', newPath); - $hitarea.attr('d', newPath); - $shadow.attr('d', newPath); + // Sichtbare Linie, Hitarea und Shadow aktualisieren (unabhängig vom Group-Typ) + $svg.find('.schematic-connection[data-connection-id="' + connIdStr + '"]').attr('d', newPath); + $svg.find('.schematic-connection-hitarea[data-connection-id="' + connIdStr + '"]').attr('d', newPath); + $svg.find('.schematic-connection-shadow[data-connection-id="' + connIdStr + '"]').attr('d', newPath); + + // Auch Shadow ohne data-Attribut (direkt vor der Group) aktualisieren + var $group = $svg.find('[data-connection-id="' + connIdStr + '"]').closest('g'); + $group.prev('.schematic-connection-shadow').attr('d', newPath); }, /** - * Finish wire drag and save + * Terminal-Position für eine Connection ermitteln. + * Gibt {x, y} zurück für den Ankerpunkt am Terminal. + */ + getConnectionTerminalAnchor: function(conn, side) { + var eq, terminals, termId, pos; + + if (side === 'source') { + eq = this.getEquipmentById(conn.fk_source); + if (!eq) return null; + terminals = this.getTerminals(eq); + termId = conn.source_terminal_id || 't2'; + pos = this.getTerminalPosition(eq, termId, terminals); + } else { + eq = this.getEquipmentById(conn.fk_target); + if (!eq) return null; + terminals = this.getTerminals(eq); + termId = conn.target_terminal_id || 't1'; + pos = this.getTerminalPosition(eq, termId, terminals); + } + + return pos ? { x: pos.x, y: pos.y } : null; + }, + + /** + * Prüft ob ein Punkt auf einem Segment liegt (mit Toleranz). + */ + isPointOnSegment: function(point, segStart, segEnd, tolerance) { + tolerance = tolerance || 5; + var isHoriz = Math.abs(segStart.y - segEnd.y) < 2; + var isVert = Math.abs(segStart.x - segEnd.x) < 2; + + if (isHoriz) { + var minX = Math.min(segStart.x, segEnd.x); + var maxX = Math.max(segStart.x, segEnd.x); + return Math.abs(point.y - segStart.y) <= tolerance && + point.x >= minX - tolerance && point.x <= maxX + tolerance; + } else if (isVert) { + var minY = Math.min(segStart.y, segEnd.y); + var maxY = Math.max(segStart.y, segEnd.y); + return Math.abs(point.x - segStart.x) <= tolerance && + point.y >= minY - tolerance && point.y <= maxY + tolerance; + } + return false; + }, + + /** + * Prüft ob ein Punkt auf irgendeinem Segment eines Pfades liegt. + * Gibt true zurück wenn der Punkt auf dem Pfad liegt. + */ + isPointOnPath: function(point, pathPoints, tolerance) { + tolerance = tolerance || 5; + for (var i = 0; i < pathPoints.length - 1; i++) { + if (this.isPointOnSegment(point, pathPoints[i], pathPoints[i + 1], tolerance)) { + return true; + } + } + return false; + }, + + /** + * Finde den nächstliegenden Punkt auf einem Pfad. + * Gibt {x, y} zurück. + */ + nearestPointOnPath: function(point, pathPoints) { + var best = null; + var bestDist = Infinity; + + for (var i = 0; i < pathPoints.length - 1; i++) { + var nearest = this.nearestPointOnSegment(pathPoints[i], pathPoints[i + 1], point.x, point.y); + var dist = Math.sqrt(Math.pow(nearest.x - point.x, 2) + Math.pow(nearest.y - point.y, 2)); + if (dist < bestDist) { + bestDist = dist; + best = nearest; + } + } + + return best || point; + }, + + /** + * Finde alle Connections deren Start- oder Endpunkt auf dem alten + * Segment lag (vor der Verschiebung). + * Gibt Array von {conn, pointIndex, point} zurück. + */ + /** + * Finde alle Connections deren freier Endpunkt (ohne Terminal) auf + * irgendeinem Segment des alten Pfads lag. + */ + findJunctionsOnPath: function(draggedConnId, oldPathPoints) { + var self = this; + var junctions = []; + var tolerance = 15; + + this.connections.forEach(function(conn) { + if (conn.id == draggedConnId) return; + if (parseInt(conn.is_rail) === 1) return; + if (!conn.path_data) return; + + var points = self.parsePathToPoints(conn.path_data); + if (points.length < 2) return; + + var hasSource = conn.fk_source && conn.fk_source != '0'; + var hasTarget = conn.fk_target && conn.fk_target != '0'; + + // Erster Punkt (Start) prüfen - nur wenn kein Terminal am Start + if (!hasSource) { + var firstOnPath = self.isPointOnPath(points[0], oldPathPoints, tolerance); + console.log('[WireDrag] Junction-Check Conn #' + conn.id + ' Start (' + + Math.round(points[0].x) + ',' + Math.round(points[0].y) + ') auf Pfad:', firstOnPath); + if (firstOnPath) { + junctions.push({ conn: conn, pointIndex: 0, point: points[0] }); + } + } + + // Letzter Punkt (Ende) prüfen - nur wenn kein Terminal am Ende + var lastIdx = points.length - 1; + if (!hasTarget) { + var lastOnPath = self.isPointOnPath(points[lastIdx], oldPathPoints, tolerance); + console.log('[WireDrag] Junction-Check Conn #' + conn.id + ' Ende (' + + Math.round(points[lastIdx].x) + ',' + Math.round(points[lastIdx].y) + ') auf Pfad:', lastOnPath); + if (lastOnPath) { + junctions.push({ conn: conn, pointIndex: lastIdx, point: points[lastIdx] }); + } + } + }); + + console.log('[WireDrag] findJunctionsOnPath: ' + junctions.length + ' gefunden von ' + + self.connections.filter(function(c) { return c.id != draggedConnId && !parseInt(c.is_rail) && c.path_data; }).length + ' geprüft'); + return junctions; + }, + + /** + * Junction-Punkte aktualisieren. + * Wenn der Junction-Punkt auf dem gezogenen Segment lag → Delta direkt anwenden. + * Wenn er auf einem anderen Segment lag → Projektion auf den neuen Pfad. + * @param {Array} junctions - Gefundene Junctions + * @param {Array} newPathPoints - Neuer Pfad (nach Verschiebung) + * @param {Object} dragInfo - {oldSegStart, oldSegEnd, isHorizontal, delta} + */ + updateJunctionPoints: function(junctions, newPathPoints, dragInfo) { + var self = this; + + junctions.forEach(function(junc) { + var points = self.parsePathToPoints(junc.conn.path_data); + if (!points[junc.pointIndex]) return; + + // Alte Position kopieren (juncPoint ist Referenz → wird sonst überschrieben) + var oldX = points[junc.pointIndex].x; + var oldY = points[junc.pointIndex].y; + var moved = false; + + console.log('[WireDrag] Junction #' + junc.conn.id + ' Pfad vorher:', junc.conn.path_data, + 'pointIndex:', junc.pointIndex, 'fk_source:', junc.conn.fk_source, 'fk_target:', junc.conn.fk_target); + + // Zuerst prüfen: Lag der Punkt auf dem gezogenen Segment? + if (dragInfo && self.isPointOnSegment({x: oldX, y: oldY}, dragInfo.oldSegStart, dragInfo.oldSegEnd, 12)) { + // Punkt lag auf dem gezogenen Segment → gleichen Delta anwenden + if (dragInfo.isHorizontal) { + points[junc.pointIndex].y = Math.round(oldY + dragInfo.delta); + } else { + points[junc.pointIndex].x = Math.round(oldX + dragInfo.delta); + } + moved = true; + console.log('[WireDrag] Junction #' + junc.conn.id + ' Delta angewandt:', + oldX + ',' + oldY, '→', + points[junc.pointIndex].x + ',' + points[junc.pointIndex].y); + } else { + // Punkt lag auf anderem Segment → prüfen ob noch auf neuem Pfad + if (self.isPointOnPath({x: oldX, y: oldY}, newPathPoints, 12)) { + // Punkt liegt noch auf der Leitung → stehen lassen + console.log('[WireDrag] Junction #' + junc.conn.id + ' bleibt stehen (noch auf Leitung)', + oldX + ',' + oldY); + return; + } + + // Nicht mehr auf Leitung → auf nächsten Punkt projizieren + var projected = self.nearestPointOnPath({x: oldX, y: oldY}, newPathPoints); + points[junc.pointIndex].x = Math.round(projected.x); + points[junc.pointIndex].y = Math.round(projected.y); + moved = true; + console.log('[WireDrag] Junction #' + junc.conn.id + ' projiziert:', + oldX + ',' + oldY, '→', + points[junc.pointIndex].x + ',' + points[junc.pointIndex].y); + } + + if (!moved) return; + + // Rechtwinkligkeit erhalten: angrenzenden Punkt anpassen + var adjIdx = (junc.pointIndex === 0) ? 1 : junc.pointIndex - 1; + if (points[adjIdx]) { + if (dragInfo.isHorizontal) { + // Y wurde verschoben - wenn angrenzendes Segment horizontal war, Y mitziehen + if (Math.abs(oldY - points[adjIdx].y) < 3) { + console.log('[WireDrag] Junction #' + junc.conn.id + ' adj[' + adjIdx + '] Y angepasst:', + points[adjIdx].y, '→', points[junc.pointIndex].y); + points[adjIdx].y = points[junc.pointIndex].y; + } + } else { + // X wurde verschoben - wenn angrenzendes Segment vertikal war, X mitziehen + if (Math.abs(oldX - points[adjIdx].x) < 3) { + console.log('[WireDrag] Junction #' + junc.conn.id + ' adj[' + adjIdx + '] X angepasst:', + points[adjIdx].x, '→', points[junc.pointIndex].x); + points[adjIdx].x = points[junc.pointIndex].x; + } + } + } + + var newPath = self.pointsToPath(points); + + // Lokal aktualisieren + junc.conn.path_data = newPath; + + // SVG direkt aktualisieren (damit es sofort sichtbar ist) + var $svg = $(self.svgElement); + $svg.find('.schematic-connection[data-connection-id="' + junc.conn.id + '"]').attr('d', newPath); + $svg.find('.schematic-connection-hitarea[data-connection-id="' + junc.conn.id + '"]').attr('d', newPath); + $svg.find('.schematic-connection-shadow[data-connection-id="' + junc.conn.id + '"]').attr('d', newPath); + + // Auf Server speichern + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'update', + connection_id: junc.conn.id, + path_data: newPath, + token: $('input[name="token"]').val() + } + }); + }); + }, + + /** + * Finish wire drag and save. + * Verankert Start/End-Punkte an den Terminal-Positionen, + * zieht Junction-Leitungen mit und speichert den neuen Pfad. */ finishWireDrag: function(mouseX, mouseY) { if (!this.wireDragMode) return; @@ -11034,27 +11773,66 @@ var points = JSON.parse(JSON.stringify(this.wireDragOriginalPoints)); var segIdx = this.wireDragSegmentIndex; + // Kompletten alten Pfad merken (VOR Verschiebung) für Junction-Suche + var oldPoints = JSON.parse(JSON.stringify(points)); + + var delta; if (this.wireDragIsHorizontal) { var deltaY = mouseY - this.wireDragStartPos.y; var newY = points[segIdx].y + deltaY; newY = Math.round(newY / this.WIRE_GRID_SIZE) * this.WIRE_GRID_SIZE; + delta = newY - points[segIdx].y; points[segIdx].y = newY; points[segIdx + 1].y = newY; } else { var deltaX = mouseX - this.wireDragStartPos.x; var newX = points[segIdx].x + deltaX; newX = Math.round(newX / this.WIRE_GRID_SIZE) * this.WIRE_GRID_SIZE; + delta = newX - points[segIdx].x; points[segIdx].x = newX; points[segIdx + 1].x = newX; } - // Clean up collinear points + // Ankerpunkte an Terminals fixieren + var conn = this.connections.find(function(c) { return c.id == this.wireDragConnId; }.bind(this)); + if (conn) { + if (conn.fk_source) { + var sourceAnchor = this.getConnectionTerminalAnchor(conn, 'source'); + if (sourceAnchor && points.length > 0) { + points[0].x = sourceAnchor.x; + points[0].y = sourceAnchor.y; + } + } + if (conn.fk_target) { + var targetAnchor = this.getConnectionTerminalAnchor(conn, 'target'); + if (targetAnchor && points.length > 1) { + points[points.length - 1].x = targetAnchor.x; + points[points.length - 1].y = targetAnchor.y; + } + } + } + + // Junction-Leitungen finden die irgendwo auf dem alten Pfad lagen + var dragInfo = { + oldSegStart: { x: oldPoints[segIdx].x, y: oldPoints[segIdx].y }, + oldSegEnd: { x: oldPoints[segIdx + 1].x, y: oldPoints[segIdx + 1].y }, + isHorizontal: this.wireDragIsHorizontal, + delta: delta + }; + var junctions = this.findJunctionsOnPath(this.wireDragConnId, oldPoints); + console.log('[WireDrag] Junctions auf altem Pfad gefunden:', junctions.length, 'delta:', delta, + 'Segment:', JSON.stringify(dragInfo.oldSegStart), '→', JSON.stringify(dragInfo.oldSegEnd)); + if (junctions.length > 0 && delta !== 0) { + this.updateJunctionPoints(junctions, points, dragInfo); + } + + // Redundante Punkte bereinigen points = this.cleanupPathPoints(points); var newPath = this.pointsToPath(points); var self = this; - // Save to server + // Auf Server speichern $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', method: 'POST', @@ -11066,21 +11844,21 @@ }, success: function(response) { if (response.success) { - // Update local data var conn = self.connections.find(function(c) { return c.id == self.wireDragConnId; }); if (conn) { conn.path_data = newPath; } self.showMessage('Leitung verschoben', 'success'); + self.renderConnections(); } else { self.showMessage('Fehler: ' + (response.error || 'Unbekannt'), 'error'); - self.renderConnections(); // Revert visual + self.renderConnections(); } self.cancelWireDrag(); }, error: function() { self.showMessage('Netzwerkfehler', 'error'); - self.renderConnections(); // Revert visual + self.renderConnections(); self.cancelWireDrag(); } }); @@ -11097,6 +11875,11 @@ this.wireDragStartPos = null; this.wireDragOriginalPoints = []; $(this.svgElement).css('cursor', ''); + + // Flag damit das nachfolgende click-Event ignoriert wird + this._wireDragJustEnded = true; + var self = this; + setTimeout(function() { self._wireDragJustEnded = false; }, 100); }, // Update an existing connection with extended path @@ -11595,6 +12378,10 @@ var self = this; $('.schematic-output-dialog').remove(); + // Vorausgewählte Phase und Farbe + var defaultPhase = existingInput ? existingInput.connection_type || 'L1' : 'L1'; + var defaultColor = existingInput && existingInput.color ? existingInput.color : (self.PHASE_COLORS[defaultPhase] || '#8B4513'); + var html = '
' + ' Anschlusspunkt (Eingang)'; - // Phase selection + // Phase-Buttons (wie im Leitungs-Dialog) html += '
'; - html += ''; - html += ''; + + html += '
'; + html += ''; + html += ''; html += '
'; // Bezeichnung (optional) @@ -11638,6 +12436,14 @@ $('body').append(html); + // Phase-Button Klick-Handler + $('.input-phase-btn').on('click', function() { + $('.input-phase-btn').css('outline', 'none'); + $(this).css('outline', '2px solid #fff'); + $('.input-phase-val').val($(this).data('phase')); + $('.input-color-val').val($(this).data('color')); + }); + $('.input-cancel-btn').on('click', function() { $('.schematic-output-dialog').remove(); }); $('.input-delete-btn').on('click', function() { var id = $(this).data('id'); @@ -11645,12 +12451,13 @@ self.deleteConnection(id); }); $('.input-save-btn').on('click', function() { - var phase = $('.input-phase').val(); + var phase = $('.input-phase-val').val(); + var color = $('.input-color-val').val(); var label = $('.input-label').val(); if (existingInput) { - self.updateInput(existingInput.id, phase, label); + self.updateInput(existingInput.id, phase, label, color); } else { - self.createInput(eqId, termId, phase, label); + self.createInput(eqId, termId, phase, label, color); } $('.schematic-output-dialog').remove(); }); @@ -11658,6 +12465,15 @@ $(document).one('keydown.inputDialog', function(e) { if (e.key === 'Escape') $('.schematic-output-dialog').remove(); }); + + // Dialog-Position anpassen + setTimeout(function() { + var $dialog = $('.schematic-output-dialog'); + var dw = $dialog.outerWidth(), dh = $dialog.outerHeight(); + var ww = $(window).width(), wh = $(window).height(); + if (x + dw > ww - 10) $dialog.css('left', (ww - dw - 10) + 'px'); + if (y + dh > wh - 10) $dialog.css('top', (wh - dh - 10) + 'px'); + }, 10); }, // Show dialog for creating an OUTPUT connection (Abgang for Verbraucher, N) @@ -11831,7 +12647,7 @@ }, // Create INPUT connection (external source -> equipment terminal) - createInput: function(eqId, termId, phase, label) { + createInput: function(eqId, termId, phase, label, color) { var self = this; $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', @@ -11843,6 +12659,7 @@ fk_target: eqId, target_terminal_id: termId, connection_type: phase, + color: color || '', output_label: label, token: $('input[name="token"]').val() }, @@ -11859,7 +12676,7 @@ }, // Update INPUT connection - updateInput: function(connId, phase, label) { + updateInput: function(connId, phase, label, color) { var self = this; $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', @@ -11868,6 +12685,7 @@ action: 'update', connection_id: connId, connection_type: phase, + color: color || '', output_label: label, token: $('input[name="token"]').val() }, @@ -12173,15 +12991,27 @@ dialogHtml += '

Verbindung bearbeiten

'; - // Connection type + // Phase/Typ als farbige Buttons + var currentType = conn.connection_type || ''; + var currentColor = conn.color || self.PHASE_COLORS[currentType] || '#3498db'; dialogHtml += '
'; - dialogHtml += ''; - dialogHtml += '
'; + + dialogHtml += ''; + dialogHtml += ''; + dialogHtml += ''; + dialogHtml += ''; // Output label dialogHtml += '
'; @@ -12189,12 +13019,6 @@ dialogHtml += '
'; - // Color - dialogHtml += '
'; - dialogHtml += ''; - dialogHtml += '
'; - // Medium type dialogHtml += '
'; dialogHtml += ''; @@ -12233,6 +13057,14 @@ $('body').append(overlayHtml).append(dialogHtml); // Bind events + // Phase-Button Klick-Handler + $('.edit-type-btn').on('click', function() { + $('.edit-type-btn').css('outline', 'none'); + $(this).css('outline', '2px solid #fff'); + $('.edit-connection-type').val($(this).data('type')); + $('.edit-connection-color').val($(this).data('color')); + }); + $('.edit-dialog-cancel, .schematic-edit-overlay').on('click', function() { self.closeEditDialog(); }); diff --git a/js/pwa.js b/js/pwa.js old mode 100644 new mode 100755