array(139, 69, 19), // Braun 'L2' => array(26, 26, 26), // Schwarz 'L3' => array(102, 102, 102), // Grau 'N' => array(0, 102, 204), // Blau 'PE' => array(39, 174, 96), // Grün 'LN' => array(139, 69, 19), // Braun '3P' => array(155, 89, 182), // Lila '3P+N' => array(52, 73, 94), // Dunkelblau ); $p = strtoupper($phase); return isset($colors[$p]) ? $colors[$p] : array(136, 136, 136); } /** * Phase-Labels parsen (PHP-Port von JS parsePhaseLabels) */ function parsePhaseLabels($phases) { if (empty($phases)) return array(); $p = strtoupper(trim($phases)); $map = array( '3P' => array('L1', 'L2', 'L3'), 'L1L2L3' => array('L1', 'L2', 'L3'), '3P+N' => array('L1', 'L2', 'L3', 'N'), '3PN' => array('L1', 'L2', 'L3', 'N'), '3P+N+PE' => array('L1', 'L2', 'L3', 'N', 'PE'), '3PNPE' => array('L1', 'L2', 'L3', 'N', 'PE'), 'L1N' => array('L1', 'N'), 'L1+N' => array('L1', 'N'), 'L1' => array('L1'), 'L2' => array('L2'), 'L3' => array('L3'), 'N' => array('N'), 'PE' => array('PE'), ); if (isset($map[$p])) return $map[$p]; if (strpos($p, '+') !== false) return explode('+', $p); if (strpos($p, ',') !== false) return array_map('trim', explode(',', $p)); return array($phases); } /** * Terminals eines Equipment ermitteln (PHP-Port von JS getTerminals) */ function getEquipmentTerminals($eq) { // terminals_config aus dem Equipment-Typ $terminalsConfig = isset($eq->terminals_config) ? $eq->terminals_config : ''; if (!empty($terminalsConfig)) { // Literale \r\n bereinigen $configStr = str_replace(array("\\r\\n", "\\r", "\\n", "\\t"), array(' ', ' ', ' ', ''), $terminalsConfig); $config = @json_decode($configStr, true); if (is_array($config) && isset($config['terminals'])) { return $config['terminals']; } if (is_array($config)) { $terminals = array(); if (isset($config['inputs'])) { foreach ($config['inputs'] as $t) { $terminals[] = array('id' => $t['id'], 'label' => $t['label'] ?? '●', 'pos' => 'top'); } } if (isset($config['outputs'])) { foreach ($config['outputs'] as $t) { $terminals[] = array('id' => $t['id'], 'label' => $t['label'] ?? '●', 'pos' => 'bottom'); } } if (!empty($terminals)) return $terminals; } } // Default-Terminals nach type_ref $typeRef = strtoupper(isset($eq->type_ref) ? $eq->type_ref : ''); $defaults = array( 'LS' => array(array('id'=>'t1','pos'=>'top'), array('id'=>'t2','pos'=>'bottom')), 'FI' => array( array('id'=>'t1','pos'=>'top'), array('id'=>'t2','pos'=>'top'), array('id'=>'t3','pos'=>'top'), array('id'=>'t4','pos'=>'top'), array('id'=>'t5','pos'=>'bottom'), array('id'=>'t6','pos'=>'bottom'), array('id'=>'t7','pos'=>'bottom'), array('id'=>'t8','pos'=>'bottom'), ), 'FI4P' => array( array('id'=>'t1','pos'=>'top'), array('id'=>'t2','pos'=>'top'), array('id'=>'t3','pos'=>'top'), array('id'=>'t4','pos'=>'top'), array('id'=>'t5','pos'=>'bottom'), array('id'=>'t6','pos'=>'bottom'), array('id'=>'t7','pos'=>'bottom'), array('id'=>'t8','pos'=>'bottom'), ), 'LS3P' => array( array('id'=>'t1','pos'=>'top'), array('id'=>'t2','pos'=>'top'), array('id'=>'t3','pos'=>'top'), array('id'=>'t4','pos'=>'bottom'), array('id'=>'t5','pos'=>'bottom'), array('id'=>'t6','pos'=>'bottom'), ), 'HS3P' => array( array('id'=>'t1','pos'=>'top'), array('id'=>'t2','pos'=>'top'), array('id'=>'t3','pos'=>'top'), array('id'=>'t4','pos'=>'bottom'), array('id'=>'t5','pos'=>'bottom'), array('id'=>'t6','pos'=>'bottom'), ), 'KLEMME' => array(array('id'=>'t1','pos'=>'top'), array('id'=>'t2','pos'=>'bottom')), ); if (isset($defaults[$typeRef])) return $defaults[$typeRef]; if (strpos($typeRef, 'FI') !== false || strpos($typeRef, 'RCD') !== false) { return (strpos($typeRef, '4P') !== false) ? $defaults['FI4P'] : $defaults['FI']; } return $defaults['LS']; } /** * WiringDiagramAnalyzer - Analysiert die Schaltplan-Daten und baut Strompfade */ class WiringDiagramAnalyzer { private $db; private $anlageId; // Rohdaten public $panels = array(); public $carriers = array(); public $allEquipment = array(); public $allConnections = array(); // Lookup-Maps private $equipmentById = array(); private $carrierById = array(); private $carrierByEquipmentId = array(); // Phase-Propagierung private $terminalPhaseMap = array(); private $terminalColorMap = array(); // Ergebnis public $circuitPaths = array(); public $hauptschalter = null; public function __construct($db, $anlageId) { $this->db = $db; $this->anlageId = (int) $anlageId; } /** * Alle Daten laden */ public function loadData() { // Panels $panelObj = new EquipmentPanel($this->db); $this->panels = $panelObj->fetchByAnlage($this->anlageId); // Carriers $carrierObj = new EquipmentCarrier($this->db); $this->carriers = $carrierObj->fetchByAnlage($this->anlageId); // Equipment + Connections pro Carrier $eqObj = new Equipment($this->db); $connObj = new EquipmentConnection($this->db); foreach ($this->carriers as $c) { $eqList = $eqObj->fetchByCarrier($c->id); $connList = $connObj->fetchByCarrier($c->id); $this->allEquipment = array_merge($this->allEquipment, $eqList); $this->allConnections = array_merge($this->allConnections, $connList); } // Connections deduplizieren (Cross-Carrier können doppelt geladen werden) $uniqueConns = array(); foreach ($this->allConnections as $c) { $uniqueConns[$c->id] = $c; } $this->allConnections = array_values($uniqueConns); // Connections mit fk_carrier=NULL aber fk_source/fk_target auf Equipment dieser Anlage // (Abgänge/Eingänge werden oft ohne fk_carrier gespeichert) $loadedConnIds = array(); foreach ($this->allConnections as $c) { $loadedConnIds[$c->id] = true; } $equipmentIds = array(); foreach ($this->allEquipment as $eq) { $equipmentIds[] = (int) $eq->id; } if (!empty($equipmentIds)) { $idList = implode(',', $equipmentIds); $sql = "SELECT c.*, src.label as source_label, src.position_te as source_pos, src.width_te as source_width,"; $sql .= " tgt.label as target_label, tgt.position_te as target_pos,"; $sql .= " bt.phases_config as busbar_phases_config"; $sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection as c"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as src ON c.fk_source = src.rowid"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as tgt ON c.fk_target = tgt.rowid"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_busbar_type as bt ON c.fk_busbar_type = bt.rowid"; $sql .= " WHERE c.fk_carrier IS NULL AND c.status = 1"; $sql .= " AND (c.fk_source IN (".$idList.") OR c.fk_target IN (".$idList."))"; $resql = $this->db->query($sql); if ($resql) { while ($obj = $this->db->fetch_object($resql)) { if (isset($loadedConnIds[$obj->rowid])) continue; $conn = new EquipmentConnection($this->db); $conn->id = $obj->rowid; $conn->fk_carrier = $obj->fk_carrier; $conn->fk_source = $obj->fk_source; $conn->fk_target = $obj->fk_target; $conn->source_terminal_id = $obj->source_terminal_id; $conn->target_terminal_id = $obj->target_terminal_id; $conn->connection_type = $obj->connection_type; $conn->color = $obj->color; $conn->output_label = $obj->output_label; $conn->output_location = isset($obj->output_location) ? $obj->output_location : null; $conn->medium_type = $obj->medium_type; $conn->medium_spec = $obj->medium_spec; $conn->medium_length = $obj->medium_length; $conn->is_rail = $obj->is_rail; $conn->path_data = $obj->path_data; $conn->bundled_terminals = $obj->bundled_terminals; $conn->status = $obj->status; $this->allConnections[] = $conn; } $this->db->free($resql); } } // Lookup-Maps bauen foreach ($this->allEquipment as $eq) { $this->equipmentById[$eq->id] = $eq; } foreach ($this->carriers as $c) { $this->carrierById[$c->id] = $c; } foreach ($this->allEquipment as $eq) { $this->carrierByEquipmentId[$eq->id] = isset($this->carrierById[$eq->fk_carrier]) ? $this->carrierById[$eq->fk_carrier] : null; } } /** * Analyse durchführen: Phase-Map bauen + Strompfade finden */ public function analyze() { $this->buildPhaseMap(); $this->findHauptschalter(); $this->buildCircuitPaths(); } /** * Hauptschalter finden (Equipment-Typ mit Ref HS*) */ private function findHauptschalter() { foreach ($this->allEquipment as $eq) { $typeRef = strtoupper($eq->type_ref ?? ''); if (strpos($typeRef, 'HS') === 0) { $this->hauptschalter = $eq; return; } } } /** * PHP-Port von JS buildTerminalPhaseMap() * Propagiert Phasen von Eingängen durch Blöcke, Leitungen und Busbars */ private function buildPhaseMap() { $this->terminalPhaseMap = array(); $this->terminalColorMap = array(); $validPhases = array('L1', 'L2', 'L3', 'N', 'PE'); // Hilfsfunktionen als Closures $setPhase = function($eqId, $termId, $phase, $color = null) use ($validPhases) { if (!isset($this->terminalPhaseMap[$eqId])) $this->terminalPhaseMap[$eqId] = array(); if (!isset($this->terminalColorMap[$eqId])) $this->terminalColorMap[$eqId] = array(); if (isset($this->terminalPhaseMap[$eqId][$termId])) return false; $this->terminalPhaseMap[$eqId][$termId] = $phase; $this->terminalColorMap[$eqId][$termId] = $color ?: '#888'; return true; }; $forcePhase = function($eqId, $termId, $phase, $color = null) { if (!isset($this->terminalPhaseMap[$eqId])) $this->terminalPhaseMap[$eqId] = array(); if (!isset($this->terminalColorMap[$eqId])) $this->terminalColorMap[$eqId] = array(); if (isset($this->terminalPhaseMap[$eqId][$termId]) && $this->terminalPhaseMap[$eqId][$termId] === $phase) return false; $this->terminalPhaseMap[$eqId][$termId] = $phase; $this->terminalColorMap[$eqId][$termId] = $color ?: '#888'; return true; }; $getColor = function($eqId, $termId) { return isset($this->terminalColorMap[$eqId][$termId]) ? $this->terminalColorMap[$eqId][$termId] : null; }; // Schritt 1: Anschlusspunkte (Inputs) als Startpunkte foreach ($this->allConnections as $conn) { if ($conn->is_rail) continue; if (!empty($conn->fk_source)) continue; if (empty($conn->fk_target) || empty($conn->target_terminal_id)) continue; $phase = strtoupper($conn->connection_type ?: ''); if (!in_array($phase, $validPhases)) continue; $rgb = getPhaseColorRGB($phase); $inputColor = !empty($conn->color) ? $conn->color : sprintf('#%02x%02x%02x', $rgb[0], $rgb[1], $rgb[2]); $setPhase($conn->fk_target, $conn->target_terminal_id, $phase, $inputColor); } // Schritt 2: Iterativ propagieren $changed = true; $iterations = 0; while ($changed && $iterations++ < 20) { $changed = false; // Block-Durchreichung (top ↔ bottom) foreach ($this->allEquipment as $eq) { $terminals = getEquipmentTerminals($eq); $topTerminals = array_values(array_filter($terminals, function($t) { return ($t['pos'] ?? '') === 'top'; })); $bottomTerminals = array_values(array_filter($terminals, function($t) { return ($t['pos'] ?? '') === 'bottom'; })); $pairCount = min(count($topTerminals), count($bottomTerminals)); for ($i = 0; $i < $pairCount; $i++) { $topId = $topTerminals[$i]['id'] ?? 't'.($i+1); $botId = $bottomTerminals[$i]['id'] ?? 't'.($i + count($topTerminals) + 1); $topPhase = $this->terminalPhaseMap[$eq->id][$topId] ?? null; $botPhase = $this->terminalPhaseMap[$eq->id][$botId] ?? null; if ($topPhase && !$botPhase) { if ($setPhase($eq->id, $botId, $topPhase, $getColor($eq->id, $topId))) $changed = true; } elseif ($botPhase && !$topPhase) { if ($setPhase($eq->id, $topId, $botPhase, $getColor($eq->id, $botId))) $changed = true; } } } // Leitungen propagieren foreach ($this->allConnections as $conn) { if ($conn->is_rail) continue; if (empty($conn->fk_source) || empty($conn->fk_target)) continue; if (empty($conn->source_terminal_id) || empty($conn->target_terminal_id)) continue; $srcPhase = $this->terminalPhaseMap[$conn->fk_source][$conn->source_terminal_id] ?? null; $tgtPhase = $this->terminalPhaseMap[$conn->fk_target][$conn->target_terminal_id] ?? null; if ($srcPhase && !$tgtPhase) { if ($setPhase($conn->fk_target, $conn->target_terminal_id, $srcPhase, $getColor($conn->fk_source, $conn->source_terminal_id))) $changed = true; } elseif ($tgtPhase && !$srcPhase) { if ($setPhase($conn->fk_source, $conn->source_terminal_id, $tgtPhase, $getColor($conn->fk_target, $conn->target_terminal_id))) $changed = true; } } // Busbar-Verteilung foreach ($this->allConnections as $busbar) { if (!$busbar->is_rail) continue; $railStart = (int) ($busbar->rail_start_te ?: 1); $railEnd = (int) ($busbar->rail_end_te ?: $railStart); $posY = (int) ($busbar->position_y ?: 0); $targetPos = ($posY === 0) ? 'top' : 'bottom'; // Phase-Labels $phaseLabels = array(); if (!empty($busbar->phases_config)) { $pc = @json_decode($busbar->phases_config, true); if (is_array($pc) && !empty($pc)) $phaseLabels = $pc; } if (empty($phaseLabels)) { $phaseLabels = parsePhaseLabels($busbar->rail_phases ?: $busbar->connection_type ?: ''); } if (empty($phaseLabels)) continue; // Eingespeiste Phasen sammeln $fedPhases = array(); $fedColors = array(); foreach ($this->allEquipment as $eq) { if ($eq->fk_carrier != $busbar->fk_carrier) continue; $eqPosTE = floatval($eq->position_te ?: 1); $eqWidthTE = floatval($eq->width_te ?: 1); if (!($eqPosTE < $railEnd + 1 && $railStart < $eqPosTE + $eqWidthTE)) continue; $terminals = getEquipmentTerminals($eq); foreach ($terminals as $term) { if (($term['pos'] ?? '') !== $targetPos) continue; $termId = $term['id'] ?? ''; $phase = $this->terminalPhaseMap[$eq->id][$termId] ?? null; if ($phase) { $fedPhases[$phase] = true; if (!isset($fedColors[$phase])) { $fedColors[$phase] = $getColor($eq->id, $termId); } } } } if (empty($fedPhases)) continue; // Excluded TEs $excludedTEs = array(); if (!empty($busbar->excluded_te)) { $excludedTEs = array_map('intval', array_filter(array_map('trim', explode(',', $busbar->excluded_te)))); } // Verteilen foreach ($this->allEquipment as $eq) { if ($eq->fk_carrier != $busbar->fk_carrier) continue; $eqPosTE = floatval($eq->position_te ?: 1); $eqWidthTE = floatval($eq->width_te ?: 1); if (!($eqPosTE < $railEnd + 1 && $railStart < $eqPosTE + $eqWidthTE)) continue; $terminals = getEquipmentTerminals($eq); $posTerminals = array_values(array_filter($terminals, function($t) use ($targetPos) { return ($t['pos'] ?? '') === $targetPos; })); foreach ($posTerminals as $idx => $term) { $col = isset($term['col']) ? $term['col'] : ($idx % max(1, $eqWidthTE)); $absoluteTE = round($eqPosTE + $col); if (in_array($absoluteTE, $excludedTEs)) continue; if ($absoluteTE < $railStart || $absoluteTE > $railEnd) continue; $teOffset = $absoluteTE - $railStart; $phase = $phaseLabels[$teOffset % count($phaseLabels)]; if (!isset($fedPhases[$phase])) continue; $phaseColor = $fedColors[$phase] ?? '#888'; $termId = $term['id'] ?? 't'.($idx+1); if ($forcePhase($eq->id, $termId, $phase, $phaseColor)) $changed = true; } } } } } /** * Strompfade bauen: Für jeden Abgang eine Spalte */ private function buildCircuitPaths() { $this->circuitPaths = array(); // Alle Abgänge (Outputs) finden foreach ($this->allConnections as $conn) { if ($conn->is_rail) continue; // Abgang = hat Source, kein Target if (empty($conn->fk_source) || !empty($conn->fk_target)) continue; // path_data = Junction-Verbindung, kein echter Abgang if (!empty($conn->path_data)) continue; $sourceEq = $this->equipmentById[$conn->fk_source] ?? null; if (!$sourceEq) continue; $carrier = $this->carrierByEquipmentId[$conn->fk_source] ?? null; if (!$carrier) continue; // Panel ermitteln $panel = null; if (!empty($carrier->fk_panel)) { foreach ($this->panels as $p) { if ($p->id == $carrier->fk_panel) { $panel = $p; break; } } } // Phase bestimmen $phase = ''; if (!empty($conn->connection_type) && in_array(strtoupper($conn->connection_type), array('L1','L2','L3','N','PE','LN','3P','3P+N'))) { $phase = strtoupper($conn->connection_type); } // Fallback: Aus Phase-Map if (empty($phase) && !empty($conn->source_terminal_id)) { $phase = $this->terminalPhaseMap[$conn->fk_source][$conn->source_terminal_id] ?? ''; } // Fallback: Erstbeste Phase vom Equipment if (empty($phase) && isset($this->terminalPhaseMap[$conn->fk_source])) { $phases = array_values($this->terminalPhaseMap[$conn->fk_source]); if (!empty($phases)) $phase = $phases[0]; } // Schutzgerät (FI/RCD) $protectionDevice = null; if (!empty($sourceEq->fk_protection)) { $protectionDevice = $this->equipmentById[$sourceEq->fk_protection] ?? null; } // Block-Label (z.B. "B16", "C32") $blockLabel = $sourceEq->getBlockLabel(); // Abgangsnummer: R{Reihe}.{Position} $reihe = ($carrier->position ?? 0) + 1; $pos = round(floatval($sourceEq->position_te)); $abgangNr = 'R'.$reihe.'.'.$pos; // Kette aufbauen (von oben nach unten) $chain = array(); // Phase-Rail $chain[] = array('type' => 'phase_rail', 'label' => $phase ?: '?'); // Schutzgerät if ($protectionDevice) { $protLabel = $protectionDevice->label ?: ($protectionDevice->type_label_short ?: 'FI'); $protBlock = $protectionDevice->getBlockLabel(); $chain[] = array( 'type' => 'protection', 'equipment' => $protectionDevice, 'label' => $protLabel, 'block_label' => $protBlock, ); } // LS-Schalter (Breaker) $chain[] = array( 'type' => 'breaker', 'equipment' => $sourceEq, 'label' => $sourceEq->label ?: ($sourceEq->type_label_short ?: 'LS'), 'block_label' => $blockLabel, ); // Verbraucher (Abgang) $chain[] = array('type' => 'consumer', 'label' => $conn->output_label ?: '-'); $this->circuitPaths[] = array( 'abgang_nr' => $abgangNr, 'output_label' => $conn->output_label ?: '-', 'output_location' => $conn->output_location ?: '', 'phase' => $phase ?: '?', 'phase_color_rgb' => getPhaseColorRGB($phase), 'medium_type' => $conn->medium_type ?: '', 'medium_spec' => $conn->medium_spec ?: '', 'medium_length' => $conn->medium_length ?: '', 'chain' => $chain, 'protection_device' => $protectionDevice, 'breaker' => $sourceEq, 'carrier' => $carrier, 'panel' => $panel, 'connection' => $conn, ); } // Sortierung: FI-Gruppe → Carrier → Position usort($this->circuitPaths, function($a, $b) { $protA = $a['protection_device'] ? $a['protection_device']->id : PHP_INT_MAX; $protB = $b['protection_device'] ? $b['protection_device']->id : PHP_INT_MAX; if ($protA !== $protB) return $protA - $protB; $carrA = $a['carrier']->position ?? 0; $carrB = $b['carrier']->position ?? 0; if ($carrA !== $carrB) return $carrA - $carrB; $posA = $a['breaker']->position_te ?? 0; $posB = $b['breaker']->position_te ?? 0; return $posA <=> $posB; }); } /** * Ergebnis: Strompfade */ public function getCircuitPaths() { return $this->circuitPaths; } /** * Abgangsverzeichnis als flache Tabelle (alle Abgänge, nicht pro Hutschiene) * * @return array Flaches Array von Zeilen */ /** * Verteilungsdaten gruppiert nach Feld (Panel) und Reihe (Carrier) * Für Kundenansicht und Technikeransicht-Tabellen * * @return array Verschachtelt: [panelId => ['panel' => obj, 'carriers' => [carrierId => ['carrier' => obj, 'paths' => [...]]]]] */ public function getVerteilungData() { $grouped = array(); foreach ($this->circuitPaths as $path) { $panel = $path['panel']; $carrier = $path['carrier']; $panelId = $panel ? $panel->id : 0; $carrierId = $carrier ? $carrier->id : 0; if (!isset($grouped[$panelId])) { $grouped[$panelId] = array( 'panel' => $panel, 'carriers' => array(), ); } if (!isset($grouped[$panelId]['carriers'][$carrierId])) { $grouped[$panelId]['carriers'][$carrierId] = array( 'carrier' => $carrier, 'paths' => array(), ); } $grouped[$panelId]['carriers'][$carrierId]['paths'][] = $path; } // Nach Panel-Position und Carrier-Position sortieren uasort($grouped, function($a, $b) { $posA = $a['panel'] ? ($a['panel']->position ?? 0) : 0; $posB = $b['panel'] ? ($b['panel']->position ?? 0) : 0; return $posA - $posB; }); foreach ($grouped as &$pData) { uasort($pData['carriers'], function($a, $b) { $posA = $a['carrier'] ? ($a['carrier']->position ?? 0) : 0; $posB = $b['carrier'] ? ($b['carrier']->position ?? 0) : 0; return $posA - $posB; }); } return $grouped; } /** * Abgangsverzeichnis als flache Tabelle (alle Abgänge, nicht pro Hutschiene) * * @return array Flaches Array von Zeilen */ public function getAbgangTabelle() { $rows = array(); foreach ($this->circuitPaths as $path) { $protLabel = ''; if ($path['protection_device']) { $pd = $path['protection_device']; $protLabel = ($pd->label ?: $pd->type_label_short ?: 'FI'); $protBlock = $pd->getBlockLabel(); if ($protBlock) $protLabel .= ' '.$protBlock; } $kabel = $path['medium_type']; if ($path['medium_spec']) $kabel .= ' '.$path['medium_spec']; if ($path['medium_length']) $kabel .= ' ('.$path['medium_length'].')'; $rows[] = array( 'abgang_nr' => $path['abgang_nr'], 'bezeichnung' => $path['output_label'], 'raeumlichkeit' => $path['output_location'], 'phase' => $path['phase'], 'absicherung' => $path['chain'][count($path['chain'])-2]['block_label'] ?? '', 'kabel' => trim($kabel), 'schutzgeraet' => $protLabel, 'bemerkung' => '', ); } return $rows; } } /** * WiringDiagramRenderer - Zeichnet den Leitungslaufplan als PDF */ class WiringDiagramRenderer { private $pdf; private $circuitPaths; private $analyzer; private $anlage; private $societe; private $user; // Seitengröße private $pageWidth; private $pageHeight; private $orientation; private $format; // Layout-Konstanten const MARGIN_LEFT = 15; const MARGIN_RIGHT = 15; const MARGIN_TOP = 12; const MIN_COLUMN_WIDTH = 25; const MAX_COLUMN_WIDTH = 50; const COLUMN_GAP = 3; const PHASE_GAP = 5; // Dynamisch berechnet private $columnWidth; // Vertikale Positionen (werden in calculateLayout berechnet) private $yPhaseL1; private $yPhaseL2; private $yPhaseL3; private $yFiTop; private $yFiBottom; private $yLsTop; private $yLsBottom; private $yConsumer; private $yCableLabel; private $yAbgangLabel; private $yAbgangNr; private $yNRail; private $yPeRail; private $maxColumnsPerPage; // Einspeisung + Hauptschalter (nur Seite 1) private $yEinspL1; private $yEinspL2; private $yEinspL3; private $yHsContactTop; private $currentPage = 0; private $totalPages = 1; public function __construct($pdf, $analyzer, $anlage, $societe, $user, $format = 'A3', $orientation = 'L') { $this->pdf = $pdf; $this->analyzer = $analyzer; $this->circuitPaths = $analyzer->getCircuitPaths(); $this->anlage = $anlage; $this->societe = $societe; $this->user = $user; $this->format = $format; $this->orientation = $orientation; // TCPDF: Kein automatischer Seitenumbruch, keine Header/Footer, kein Credit-Link $this->pdf->SetAutoPageBreak(false, 0); $this->pdf->setPrintHeader(false); $this->pdf->setPrintFooter(false); // "Powered by TCPDF" Credit-Link auf letzter Seite deaktivieren $ref = new ReflectionProperty(get_class($this->pdf), 'tcpdflink'); $ref->setAccessible(true); $ref->setValue($this->pdf, false); // Hack-Font registrieren (Monospace, technische Pläne) if (class_exists('TCPDF_FONTS')) { $fontDir = '/usr/share/fonts/TTF/'; foreach (array('Hack-Regular.ttf', 'Hack-Bold.ttf', 'Hack-Italic.ttf', 'Hack-BoldItalic.ttf') as $f) { if (file_exists($fontDir.$f)) { TCPDF_FONTS::addTTFfont($fontDir.$f, 'TrueTypeUnicode', '', 96); } } } // Seitengröße if ($format == 'A3') { $this->pageWidth = 420; $this->pageHeight = 297; } else { $this->pageWidth = 297; $this->pageHeight = 210; } if ($orientation == 'P') { $tmp = $this->pageWidth; $this->pageWidth = $this->pageHeight; $this->pageHeight = $tmp; } $this->calculateLayout(); } /** * Seitenunabhängige Layout-Werte berechnen */ private function calculateLayout() { // Einspeisung (primäre Phasenleiter, nur Seite 1) $this->yEinspL1 = self::MARGIN_TOP + 3; $this->yEinspL2 = $this->yEinspL1 + 3; $this->yEinspL3 = $this->yEinspL2 + 3; // Hauptschalter-Kontakte (zwischen Einspeisung und Sammelschiene) $this->yHsContactTop = $this->yEinspL3 + 5; // N und PE werden in setPageLayout() relativ zum Content positioniert // Max Spalten pro Seite (mit minimaler Breite berechnen) $usableWidth = $this->pageWidth - self::MARGIN_LEFT - self::MARGIN_RIGHT - 30; $this->maxColumnsPerPage = max(1, floor($usableWidth / (self::MIN_COLUMN_WIDTH + self::COLUMN_GAP))); // Gesamtseiten berechnen $totalPaths = count($this->circuitPaths); $this->totalPages = max(1, ceil($totalPaths / $this->maxColumnsPerPage)); // Dynamische Spaltenbreite: Verfügbaren Platz gleichmäßig verteilen $colsOnPage = min($totalPaths, $this->maxColumnsPerPage); if ($colsOnPage > 0) { $this->columnWidth = min(self::MAX_COLUMN_WIDTH, max(self::MIN_COLUMN_WIDTH, ($usableWidth - ($colsOnPage - 1) * self::COLUMN_GAP) / $colsOnPage)); } else { $this->columnWidth = self::MIN_COLUMN_WIDTH; } // Initiales Layout (Seite 1) $hasHS = ($this->analyzer->hauptschalter !== null); $this->setPageLayout(true, $hasHS); } /** * Seitenspezifische Y-Positionen berechnen * Seite 1 mit HS: Sammelschiene tiefer (Platz für Einspeisung + HS) * Seite 2+: Sammelschiene oben */ private function setPageLayout($isFirstPage, $hasHS) { if ($isFirstPage && $hasHS) { // Sammelschiene unterhalb des Hauptschalters $contactLen = 7; $this->yPhaseL1 = $this->yHsContactTop + $contactLen + 5; } else { // Sammelschiene direkt oben $this->yPhaseL1 = self::MARGIN_TOP + 15; } $this->yPhaseL2 = $this->yPhaseL1 + self::PHASE_GAP; $this->yPhaseL3 = $this->yPhaseL2 + self::PHASE_GAP; // Dynamische vertikale Verteilung: Verfügbaren Platz bis Titelfeld nutzen $titleBlockTop = $this->pageHeight - 66; $availableHeight = $titleBlockTop - $this->yPhaseL3 - 10; // 10mm Puffer // Feste Element-Höhen $fiHeight = 22; // FI/RCD-Symbol $lsHeight = 18; // LS-Symbol $labelBlock = 18; // Kabel + Abgang-Label + Nr (kompakt) $npeGap = self::PHASE_GAP; // N-PE Abstand $fixedContent = $fiHeight + $lsHeight + $labelBlock + $npeGap; // Gewichtete Gaps: Verbindungslinien bekommen mehr Platz, N/PE weniger // Gewichte: L3→FI(3), FI→LS(2), LS→Pfeil(3), Labels→N(1) = Summe 9 $totalWeight = 9; $remainingSpace = max(0, $availableHeight - $fixedContent); $unit = $remainingSpace / $totalWeight; $gapPhaseToFi = max(10, $unit * 3); // Lange Leitung Phase → FI $gapFiToLs = max(6, $unit * 2); // Kurze Verbindung FI → LS $gapLsToConsumer = max(8, $unit * 3); // Leitung LS → Verbraucher $gapLabelsToN = max(5, $unit * 1); // Kompakter Abstand Labels → N $this->yFiTop = $this->yPhaseL3 + $gapPhaseToFi; $this->yFiBottom = $this->yFiTop + $fiHeight; $this->yLsTop = $this->yFiBottom + $gapFiToLs; $this->yLsBottom = $this->yLsTop + $lsHeight; $this->yConsumer = $this->yLsBottom + $gapLsToConsumer; $this->yCableLabel = $this->yConsumer + 6; $this->yAbgangLabel = $this->yCableLabel + 7; $this->yAbgangNr = $this->yAbgangLabel + 5; // N und PE unter den Abgängen $this->yNRail = $this->yAbgangNr + $gapLabelsToN; $this->yPeRail = $this->yNRail + $npeGap; // Sicherheit: Nicht ins Titelfeld zeichnen if ($this->yPeRail + 5 > $titleBlockTop) { $this->yNRail = $titleBlockTop - 10; $this->yPeRail = $this->yNRail + $npeGap; } } /** * Leitungslaufplan zeichnen (alle Seiten) * Seite 1: Einspeisung → Hauptschalter → Sammelschiene → Strompfade * Seite 2+: Sammelschiene → Strompfade */ public function render() { $totalPaths = count($this->circuitPaths); $hasHS = ($this->analyzer->hauptschalter !== null); if ($totalPaths === 0 && !$hasHS) { $this->pdf->AddPage($this->orientation, array($this->pageWidth, $this->pageHeight)); $this->pdf->SetFont('hack', 'B', 14); $this->pdf->SetTextColor(100, 100, 100); $this->pdf->Text(self::MARGIN_LEFT, $this->pageHeight / 2, 'Keine Abgänge konfiguriert'); $this->drawTitleBlock(1, 1); return; } $pathIndex = 0; $pageNum = 0; while ($pathIndex < $totalPaths) { $pageNum++; $isFirstPage = ($pageNum === 1); // Seitenspezifische Y-Positionen berechnen $this->setPageLayout($isFirstPage, $hasHS); $this->pdf->AddPage($this->orientation, array($this->pageWidth, $this->pageHeight)); // Spalten für diese Seite $pagePaths = array_slice($this->circuitPaths, $pathIndex, $this->maxColumnsPerPage); $numCols = count($pagePaths); // Dynamische Spaltenbreite für diese Seite $colW = $this->columnWidth; $totalContentWidth = $numCols * ($colW + self::COLUMN_GAP) - self::COLUMN_GAP; $availWidth = $this->pageWidth - self::MARGIN_LEFT - self::MARGIN_RIGHT - 30; // Zentriert positionieren $startX = self::MARGIN_LEFT + 30 + ($availWidth - $totalContentWidth) / 2; $endX = $startX + $totalContentWidth; // Einspeisung + Hauptschalter (nur Seite 1, zentriert über Strompfaden) if ($isFirstPage && $hasHS) { $contentCenter = $startX + $totalContentWidth / 2; $this->drawEinspeisungUndHauptschalter($contentCenter); } // Sammelschiene-Labels links $this->drawPhaseLabels(); // Sammelschiene (L1, L2, L3) über volle Breite $railStartX = self::MARGIN_LEFT + 25; $railEndX = $this->pageWidth - self::MARGIN_RIGHT; $this->drawPhaseRails($railStartX, $railEndX); // N und PE unten über volle Breite $this->drawNPeRails($railStartX, $railEndX); // FI-Gruppen identifizieren für Trennlinien $lastProtId = null; // Strompfad-Spalten zeichnen for ($col = 0; $col < $numCols; $col++) { $path = $pagePaths[$col]; $x = $startX + $col * ($colW + self::COLUMN_GAP) + $colW / 2; // FI-Gruppen-Trenner $currentProtId = $path['protection_device'] ? $path['protection_device']->id : 0; if ($lastProtId !== null && $lastProtId !== $currentProtId && $col > 0) { $sepX = $x - ($colW + self::COLUMN_GAP) / 2; $this->pdf->SetLineStyle(array('width' => 0.2, 'dash' => '2,2', 'color' => array(180, 180, 180))); $this->pdf->Line($sepX, $this->yPhaseL1 - 5, $sepX, $this->yPeRail + 5); $this->pdf->SetLineStyle(array('dash' => 0)); } $lastProtId = $currentProtId; $this->drawCircuitColumn($path, $x); } // Mini-Legende auf Seite 1 (unten links, neben dem Titelfeld) if ($isFirstPage) { $this->drawMiniLegende(); } // Titelfeld $this->drawTitleBlock($pageNum, $this->totalPages + 1); // +1 für Tabelle $pathIndex += $numCols; } } /** * Phase-Labels links zeichnen */ private function drawPhaseLabels() { $x = self::MARGIN_LEFT; $this->pdf->SetFont('hack', 'B', 9); // L1 $rgb = getPhaseColorRGB('L1'); $this->pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->Text($x, $this->yPhaseL1 - 1, 'L1'); // L2 $rgb = getPhaseColorRGB('L2'); $this->pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->Text($x, $this->yPhaseL2 - 1, 'L2'); // L3 $rgb = getPhaseColorRGB('L3'); $this->pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->Text($x, $this->yPhaseL3 - 1, 'L3'); // N $rgb = getPhaseColorRGB('N'); $this->pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->Text($x, $this->yNRail - 1, 'N'); // PE $rgb = getPhaseColorRGB('PE'); $this->pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->Text($x, $this->yPeRail - 1, 'PE'); $this->pdf->SetTextColor(0, 0, 0); } /** * Kompakte Legende unten links auf Seite 1 (neben dem Titelfeld) */ private function drawMiniLegende() { $titleBlockWidth = 180; $titleBlockX = $this->pageWidth - $titleBlockWidth - 10; $titleBlockY = $this->pageHeight - 56 - 10; // Legende links neben dem Titelfeld $legendX = self::MARGIN_LEFT; $legendY = $titleBlockY; $legendWidth = $titleBlockX - $legendX - 5; // Phasenfarben als horizontale Reihe $this->pdf->SetFont('hack', '', 5); $phaseList = array( 'L1' => 'Braun', 'L2' => 'Schwarz', 'L3' => 'Grau', 'N' => 'Blau', 'PE' => 'Grün-Gelb', ); $px = $legendX; $py = $legendY + 2; foreach ($phaseList as $phase => $farbe) { $rgb = getPhaseColorRGB($phase); $this->pdf->SetFillColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->Rect($px, $py, 8, 3, 'F'); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->Text($px + 9, $py + 1.5, $phase); $px += 18; } // Symbol-Erklärungen darunter (Zeile 2) $py += 7; $this->pdf->SetFont('hack', '', 4.5); $this->pdf->SetTextColor(100, 100, 100); // LS-Symbol mini $this->pdf->SetDrawColor(0, 0, 0); $this->pdf->SetLineWidth(0.2); $sx = $legendX + 2; $this->pdf->Line($sx, $py, $sx, $py + 2); $this->pdf->Line($sx, $py + 2, $sx + 2, $py + 4); $this->pdf->Rect($sx - 1, $py + 4, 2, 1.5, 'D'); $this->pdf->Line($sx, $py + 5.5, $sx, $py + 7); $this->pdf->Text($sx + 4, $py + 3, 'LS-Schalter'); // FI-Symbol mini $sx = $legendX + 32; $this->pdf->Rect($sx - 2, $py, 4, 7, 'D'); $this->pdf->Circle($sx, $py + 3.5, 1.5, 0, 360, 'D'); $this->pdf->Text($sx + 4, $py + 3, 'FI/RCD'); // Abgang-Pfeil mini $sx = $legendX + 58; $this->pdf->SetFillColor(0, 0, 0); $this->pdf->Polygon(array($sx - 1.5, $py + 1, $sx + 1.5, $py + 1, $sx, $py + 4), 'F'); $this->pdf->Text($sx + 4, $py + 2, 'Abgang'); // Norm-Hinweis (Zeile 3) $py += 10; $this->pdf->SetFont('hack', 'I', 4); $this->pdf->SetTextColor(140, 140, 140); $this->pdf->Text($legendX, $py, 'DIN EN 61082 / DIN EN 81346 / DIN EN 60617'); } /** * Horizontale Phasenleiter oben (L1, L2, L3) */ private function drawPhaseRails($startX, $endX) { $this->pdf->SetLineWidth(0.5); $phases = array('L1' => $this->yPhaseL1, 'L2' => $this->yPhaseL2, 'L3' => $this->yPhaseL3); foreach ($phases as $phase => $y) { $rgb = getPhaseColorRGB($phase); $this->pdf->SetDrawColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->Line($startX, $y, $endX, $y); } } /** * N und PE Leiter unten */ private function drawNPeRails($startX, $endX) { $this->pdf->SetLineWidth(0.5); $rgb = getPhaseColorRGB('N'); $this->pdf->SetDrawColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->Line($startX, $this->yNRail, $endX, $this->yNRail); $rgb = getPhaseColorRGB('PE'); $this->pdf->SetDrawColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->Line($startX, $this->yPeRail, $endX, $this->yPeRail); } /** * Einspeisung + Hauptschalter zeichnen (nur Seite 1) * 3 kurze Einspeise-Phasenleiter oben, darunter 3-poliger HS mit * mechanischer Kopplung, verbunden mit der Sammelschiene darunter. */ private function drawEinspeisungUndHauptschalter($centerX = null) { $hs = $this->analyzer->hauptschalter; if (!$hs) return; $spacing = 6; // Abstand zwischen den 3 HS-Polen $hsCenter = $centerX ?: (self::MARGIN_LEFT + 30); // Zentriert über Strompfaden $contactLen = 7; $poleX = array( 'L1' => $hsCenter - $spacing, 'L2' => $hsCenter, 'L3' => $hsCenter + $spacing, ); $einspY = array( 'L1' => $this->yEinspL1, 'L2' => $this->yEinspL2, 'L3' => $this->yEinspL3, ); $sammelY = array( 'L1' => $this->yPhaseL1, 'L2' => $this->yPhaseL2, 'L3' => $this->yPhaseL3, ); // "Einspeisung" Label (links neben der Schaltgruppe) $this->pdf->SetFont('hack', '', 5); $this->pdf->SetTextColor(120, 120, 120); $this->pdf->Text($hsCenter - $spacing - 22, $this->yEinspL2 - 1, 'Einspeisung'); // Einspeisung-Rails (kurze Linien) + HS-Kontakte + Verbindung zur Sammelschiene $einspStartX = $hsCenter - $spacing - 12; $einspEndX = $hsCenter + $spacing + 12; $couplingPoints = array(); $this->pdf->SetLineWidth(0.5); foreach (array('L1', 'L2', 'L3') as $phase) { $rgb = getPhaseColorRGB($phase); $y = $einspY[$phase]; $px = $poleX[$phase]; // Einspeisung-Rail (kurze horizontale Linie) $this->pdf->SetDrawColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->Line($einspStartX, $y, $einspEndX, $y); // Phase-Label auf der Einspeisung $this->pdf->SetFont('hack', '', 5); $this->pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->Text($einspStartX - 7, $y - 1, $phase); // Anschluss-Punkt auf Einspeisung $this->pdf->Circle($px, $y, 1, 0, 360, 'F', array(), array($rgb[0], $rgb[1], $rgb[2])); // Vertikale Linie Einspeisung → Schaltkontakt $this->pdf->SetDrawColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->SetLineWidth(0.4); $this->pdf->Line($px, $y, $px, $this->yHsContactTop); // Schaltkontakt-Symbol (DIN EN 60617) $this->pdf->SetDrawColor(0, 0, 0); $this->pdf->SetLineWidth(0.3); $this->pdf->Circle($px, $this->yHsContactTop, 0.8, 0, 360, 'F', array(), array(0, 0, 0)); $this->pdf->Line($px, $this->yHsContactTop, $px + 3, $this->yHsContactTop + $contactLen); $this->pdf->Circle($px, $this->yHsContactTop + $contactLen + 1, 0.8, 0, 360, 'F', array(), array(0, 0, 0)); // Vertikale Linie Schaltkontakt → Sammelschiene $this->pdf->SetDrawColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->SetLineWidth(0.4); $this->pdf->Line($px, $this->yHsContactTop + $contactLen + 1, $px, $sammelY[$phase]); // Anschluss-Punkt auf Sammelschiene $this->pdf->Circle($px, $sammelY[$phase], 1, 0, 360, 'F', array(), array($rgb[0], $rgb[1], $rgb[2])); // Kopplungspunkt merken (Mitte des Schaltkontakts) $couplingPoints[] = array('x' => $px + 1.5, 'y' => $this->yHsContactTop + $contactLen * 0.4); } // Mechanische Kopplung (gestrichelte Linie durch die Schaltkontakte) if (count($couplingPoints) >= 2) { $this->pdf->SetLineStyle(array('width' => 0.3, 'dash' => '1.5,1', 'color' => array(0, 0, 0))); for ($j = 0; $j < count($couplingPoints) - 1; $j++) { $this->pdf->Line( $couplingPoints[$j]['x'], $couplingPoints[$j]['y'], $couplingPoints[$j+1]['x'], $couplingPoints[$j+1]['y'] ); } $this->pdf->SetLineStyle(array('dash' => 0)); } // HS-Bezeichnung links (z.B. "Q0") $label = $hs->label ?: 'Q0'; $this->pdf->SetFont('hack', 'B', 7); $this->pdf->SetTextColor(0, 0, 0); $midY = $this->yHsContactTop + $contactLen / 2; $this->pdf->Text($poleX['L1'] - 12, $midY, $label); // Block-Label rechts (z.B. "63A 3P") $blockLabel = method_exists($hs, 'getBlockLabel') ? $hs->getBlockLabel() : ''; if ($blockLabel) { $this->pdf->SetFont('hack', '', 6); $this->pdf->Text($poleX['L3'] + 5, $midY, $blockLabel); } // Typ-Bezeichnung links unter der HS-Bezeichnung (nicht im Symbol-Bereich) $typeLabel = $hs->type_label_short ?? ($hs->type_label ?? ''); if ($typeLabel) { $this->pdf->SetFont('hack', '', 5); $this->pdf->SetTextColor(100, 100, 100); $this->pdf->Text($poleX['L1'] - 12, $midY + 5, $typeLabel); } $this->pdf->SetTextColor(0, 0, 0); } /** * Eine Strompfad-Spalte zeichnen */ private function drawCircuitColumn($path, $x) { $phase = $path['phase']; $phaseRGB = $path['phase_color_rgb']; $conn = $path['connection']; // 3-Phasen erkennen: bundled_terminals = 'all' oder Phase = 3P/3P+N/3P+N+PE $is3Phase = (!empty($conn->bundled_terminals) && $conn->bundled_terminals === 'all') || in_array($phase, array('3P', '3P+N', '3P+N+PE')); $hasFI = !empty($path['protection_device']); $topTarget = $hasFI ? $this->yFiTop : $this->yLsTop; $colW = $this->columnWidth; if ($is3Phase) { // === Drehstrom: 3 Linien von L1, L2, L3 konvergieren zum LS === $phaseSpacing = 3; // Horizontaler Abstand der 3 Pole $mergeY = $this->yPhaseL3 + ($topTarget - $this->yPhaseL3) * 0.3; // Konvergenz-Punkt foreach (array('L1' => $this->yPhaseL1, 'L2' => $this->yPhaseL2, 'L3' => $this->yPhaseL3) as $ph => $pY) { $rgb = getPhaseColorRGB($ph); $this->pdf->SetDrawColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->SetLineWidth(0.4); // Punkt auf Phase-Rail $this->pdf->Circle($x, $pY, 1, 0, 360, 'F', array(), array($rgb[0], $rgb[1], $rgb[2])); // Diagonale Linie von Phase-Rail zum Konvergenz-Punkt $offset = ($ph === 'L1') ? -$phaseSpacing : (($ph === 'L3') ? $phaseSpacing : 0); $this->pdf->Line($x, $pY, $x + $offset, $mergeY); // Vertikale Linie vom Konvergenz-Punkt zum FI/LS $this->pdf->Line($x + $offset, $mergeY, $x + $offset, $topTarget); } // FI/RCD if ($hasFI) { $this->drawRCDSymbol($x, $this->yFiTop, $path['protection_device']); // 3 Linien FI → LS foreach (array('L1', 'L2', 'L3') as $i => $ph) { $rgb = getPhaseColorRGB($ph); $this->pdf->SetDrawColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->SetLineWidth(0.4); $offset = ($i - 1) * $phaseSpacing; $this->pdf->Line($x + $offset, $this->yFiBottom, $x + $offset, $this->yLsTop); } } // 3-poliger LS-Schalter $this->draw3PhaseBreakerSymbol($x, $this->yLsTop, $path['breaker'], $path['chain'], $phaseSpacing); // 3 Linien LS → konvergieren → 1 Linie zum Verbraucher $mergeYBottom = $this->yLsBottom + ($this->yConsumer - $this->yLsBottom) * 0.4; foreach (array('L1', 'L2', 'L3') as $i => $ph) { $rgb = getPhaseColorRGB($ph); $this->pdf->SetDrawColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->SetLineWidth(0.4); $offset = ($i - 1) * $phaseSpacing; $this->pdf->Line($x + $offset, $this->yLsBottom, $x, $mergeYBottom); } // Einzelne Linie zum Verbraucher $this->pdf->SetDrawColor($phaseRGB[0], $phaseRGB[1], $phaseRGB[2]); $this->pdf->SetLineWidth(0.4); $this->pdf->Line($x, $mergeYBottom, $x, $this->yConsumer); } else { // === Einphasig: Eine Linie von der Phase === $phaseY = $this->yPhaseL1; if ($phase === 'L2') $phaseY = $this->yPhaseL2; elseif ($phase === 'L3') $phaseY = $this->yPhaseL3; $this->pdf->SetDrawColor($phaseRGB[0], $phaseRGB[1], $phaseRGB[2]); $this->pdf->SetLineWidth(0.4); // Punkt auf Phase-Rail $this->pdf->Circle($x, $phaseY, 1, 0, 360, 'F', array(), array($phaseRGB[0], $phaseRGB[1], $phaseRGB[2])); // Vertikale Linie Phase → FI/LS $this->pdf->Line($x, $phaseY, $x, $topTarget); // FI/RCD if ($hasFI) { $this->drawRCDSymbol($x, $this->yFiTop, $path['protection_device']); $this->pdf->SetDrawColor($phaseRGB[0], $phaseRGB[1], $phaseRGB[2]); $this->pdf->SetLineWidth(0.4); $this->pdf->Line($x, $this->yFiBottom, $x, $this->yLsTop); } // LS-Schalter (einpolig) $this->drawBreakerSymbol($x, $this->yLsTop, $path['breaker'], $path['chain']); // Linie LS → Verbraucher $this->pdf->SetDrawColor($phaseRGB[0], $phaseRGB[1], $phaseRGB[2]); $this->pdf->SetLineWidth(0.4); $this->pdf->Line($x, $this->yLsBottom, $x, $this->yConsumer); } // Abgang-Pfeil (für beide: einphasig und 3-phasig) $this->pdf->SetFillColor($phaseRGB[0], $phaseRGB[1], $phaseRGB[2]); $arrowSize = 3; $this->pdf->Polygon(array( $x - $arrowSize, $this->yConsumer - $arrowSize, $x + $arrowSize, $this->yConsumer - $arrowSize, $x, $this->yConsumer + 1, ), 'F'); // Gestrichelte Linie Abgang → N-Leiter $this->pdf->SetLineStyle(array('width' => 0.15, 'dash' => '1,2', 'color' => array(180, 180, 180))); $this->pdf->Line($x, $this->yConsumer + 2, $x, $this->yNRail); $this->pdf->SetLineStyle(array('dash' => 0)); // Kabelbezeichnung $this->pdf->SetFont('hack', '', 6); $this->pdf->SetTextColor(100, 100, 100); $cableText = $path['medium_type']; if ($path['medium_spec']) $cableText .= "\n".$path['medium_spec']; if (!empty($cableText)) { $this->pdf->SetXY($x - $colW/2, $this->yCableLabel); $this->pdf->MultiCell($colW, 3, $cableText, 0, 'C'); } // Abgang-Label (Verbraucher-Name) $this->pdf->SetFont('hack', 'B', 7); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->SetXY($x - $colW/2, $this->yAbgangLabel); $this->pdf->MultiCell($colW, 3, $path['output_label'], 0, 'C'); } /** * LS-Schalter Symbol (vereinfacht) */ private function drawBreakerSymbol($x, $y, $eq, $chain) { $this->pdf->SetDrawColor(0, 0, 0); $this->pdf->SetLineWidth(0.3); // Vertikale Linie oben $this->pdf->Line($x, $y, $x, $y + 4); // Schaltkontakt (schräge Linie) $this->pdf->Line($x, $y + 4, $x + 4, $y + 8); // Auslöser (kleines Rechteck) $this->pdf->SetFillColor(255, 255, 255); $this->pdf->Rect($x - 2, $y + 8, 4, 3, 'DF'); // Vertikale Linie unten $this->pdf->Line($x, $y + 11, $x, $y + 18); // Equipment-Label rechts oben (z.B. "R2.1") — deutlich sichtbar $label = $eq->label ?: ''; if ($label) { $this->pdf->SetFont('hack', 'B', 8); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->Text($x + 5, $y + 5, $label); } // Block-Label rechts darunter (z.B. "B16") $blockLabel = ''; foreach ($chain as $c) { if ($c['type'] === 'breaker') { $blockLabel = $c['block_label'] ?? ''; break; } } if ($blockLabel) { $this->pdf->SetFont('hack', '', 6); $this->pdf->SetTextColor(80, 80, 80); $this->pdf->Text($x + 5, $y + 9, $blockLabel); } } /** * 3-poliger LS-Schalter (Drehstrom) * 3 parallele Schaltkontakte mit mechanischer Kopplung */ private function draw3PhaseBreakerSymbol($x, $y, $eq, $chain, $phaseSpacing = 3) { $contactLen = 7; $couplingPoints = array(); // 3 Pole zeichnen (L1=links, L2=mitte, L3=rechts) foreach (array('L1', 'L2', 'L3') as $i => $ph) { $rgb = getPhaseColorRGB($ph); $offset = ($i - 1) * $phaseSpacing; $px = $x + $offset; $this->pdf->SetDrawColor(0, 0, 0); $this->pdf->SetLineWidth(0.3); // Eingangs-Punkt oben $this->pdf->Line($px, $y, $px, $y + 4); // Schaltkontakt (schräge Linie) $this->pdf->Line($px, $y + 4, $px + 3, $y + 8); // Auslöser (kleines Rechteck) $this->pdf->SetFillColor(255, 255, 255); $this->pdf->Rect($px - 1.5, $y + 8, 3, 2.5, 'DF'); // Ausgangs-Linie unten $this->pdf->Line($px, $y + 10.5, $px, $y + 18); // Kopplungspunkt merken $couplingPoints[] = array('x' => $px + 1.5, 'y' => $y + 4 + $contactLen * 0.4); } // Mechanische Kopplung (gestrichelte Linie durch die Schaltkontakte) if (count($couplingPoints) >= 2) { $this->pdf->SetLineStyle(array('width' => 0.25, 'dash' => '1.5,1', 'color' => array(0, 0, 0))); for ($j = 0; $j < count($couplingPoints) - 1; $j++) { $this->pdf->Line( $couplingPoints[$j]['x'], $couplingPoints[$j]['y'], $couplingPoints[$j+1]['x'], $couplingPoints[$j+1]['y'] ); } $this->pdf->SetLineStyle(array('dash' => 0)); } // Equipment-Label rechts oben (z.B. "R3.1") $label = $eq->label ?: ''; if ($label) { $this->pdf->SetFont('hack', 'B', 8); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->Text($x + $phaseSpacing + 5, $y + 5, $label); } // Block-Label rechts darunter (z.B. "LS 3P") $blockLabel = ''; foreach ($chain as $c) { if ($c['type'] === 'breaker') { $blockLabel = $c['block_label'] ?? ''; break; } } if ($blockLabel) { $this->pdf->SetFont('hack', '', 6); $this->pdf->SetTextColor(80, 80, 80); $this->pdf->Text($x + $phaseSpacing + 5, $y + 9, $blockLabel); } } /** * FI/RCD Symbol (vereinfacht) */ private function drawRCDSymbol($x, $y, $eq) { $this->pdf->SetDrawColor(0, 0, 0); $this->pdf->SetLineWidth(0.3); $w = 16; $h = 20; // Umrandung $this->pdf->SetFillColor(255, 255, 255); $this->pdf->Rect($x - $w/2, $y, $w, $h, 'DF'); // Differenzstrom-Symbol (Kreis) $this->pdf->Circle($x, $y + $h/2, 4, 0, 360, 'D'); // Vertikale Linie durch Kreis (Auslöser) $this->pdf->Line($x, $y + $h/2 - 4, $x, $y + $h/2 + 4); // Block-Label (z.B. "40A 30mA") $blockLabel = $eq->getBlockLabel(); $this->pdf->SetFont('hack', '', 5); $this->pdf->SetTextColor(0, 0, 0); if ($blockLabel) { $this->pdf->SetXY($x - $w/2, $y + $h - 5); $this->pdf->Cell($w, 4, $blockLabel, 0, 0, 'C'); } // Equipment-Label links (z.B. "Q1") $label = $eq->label ?: ''; if ($label) { $this->pdf->SetFont('hack', '', 6); $this->pdf->SetTextColor(80, 80, 80); $this->pdf->Text($x - $w/2 - 10, $y + $h/2, $label); } // Ein/Ausgangs-Linien $this->pdf->SetDrawColor(0, 0, 0); $this->pdf->Line($x, $y - 2, $x, $y); $this->pdf->Line($x, $y + $h, $x, $y + $h + 2); } /** * Abgangsverzeichnis als einzelne Tabelle zeichnen */ public function renderAbgangTabelle() { $rows = $this->analyzer->getAbgangTabelle(); if (empty($rows)) return; // Spaltenbreiten (volle Seitenbreite nutzen) $totalWidth = $this->pageWidth - self::MARGIN_LEFT - self::MARGIN_RIGHT; $colWidths = array( round($totalWidth * 0.06), // Abg.Nr. round($totalWidth * 0.22), // Bezeichnung round($totalWidth * 0.06), // Phase round($totalWidth * 0.10), // Absicherung round($totalWidth * 0.18), // Kabel round($totalWidth * 0.18), // Schutzgerät ); $colWidths[] = $totalWidth - array_sum($colWidths); // Bemerkung = Rest $colHeaders = array('Abg.Nr.', 'Bezeichnung', 'Phase', 'Absicherung', 'Kabel', 'Schutzgerät', 'Bemerkung'); $this->pdf->AddPage($this->orientation, array($this->pageWidth, $this->pageHeight)); $y = self::MARGIN_TOP; // Titel mit Unterstrich $this->pdf->SetFont('hack', 'B', 14); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->Text(self::MARGIN_LEFT, $y + 6, 'ABGANGSVERZEICHNIS'); $y += 10; // Untertitel: Anlage + HS-Info $subText = $this->anlage->label; $hs = $this->analyzer->hauptschalter; if ($hs) { $hsLabel = $hs->label ?: 'Hauptschalter'; $hsBlock = method_exists($hs, 'getBlockLabel') ? $hs->getBlockLabel() : ''; if ($hsBlock) $hsLabel .= ' '.$hsBlock; $subText .= ' | Vorsicherung: '.$hsLabel; } $this->pdf->SetFont('hack', '', 9); $this->pdf->SetTextColor(80, 80, 80); $this->pdf->Text(self::MARGIN_LEFT, $y + 4, $subText); $this->pdf->SetTextColor(0, 0, 0); // Trennlinie $this->pdf->SetDrawColor(0, 0, 0); $this->pdf->SetLineWidth(0.5); $y += 7; $this->pdf->Line(self::MARGIN_LEFT, $y, self::MARGIN_LEFT + $totalWidth, $y); $y += 5; // Spalten-Header $headerHeight = 9; $rowHeight = 8; $this->pdf->SetFont('hack', 'B', 8); $this->pdf->SetFillColor(60, 60, 60); $this->pdf->SetTextColor(255, 255, 255); $x = self::MARGIN_LEFT; for ($i = 0; $i < count($colHeaders); $i++) { $this->pdf->SetXY($x, $y); $this->pdf->Cell($colWidths[$i], $headerHeight, $colHeaders[$i], 1, 0, 'C', true); $x += $colWidths[$i]; } $y += $headerHeight; $this->pdf->SetTextColor(0, 0, 0); // Datenzeilen $this->pdf->SetFont('hack', '', 8); foreach ($rows as $rowIdx => $row) { if ($rowIdx % 2 === 0) $this->pdf->SetFillColor(245, 245, 245); else $this->pdf->SetFillColor(255, 255, 255); // Phasenfarbe für Phase-Zelle $phaseRGB = getPhaseColorRGB($row['phase']); $x = self::MARGIN_LEFT; $cells = array( $row['abgang_nr'], $row['bezeichnung'], $row['phase'], $row['absicherung'], $row['kabel'], $row['schutzgeraet'], $row['bemerkung'], ); for ($i = 0; $i < count($cells); $i++) { $this->pdf->SetXY($x, $y); $align = ($i === 2) ? 'C' : 'L'; // Phase-Zelle farbig hervorheben if ($i === 2) { $this->pdf->SetTextColor($phaseRGB[0], $phaseRGB[1], $phaseRGB[2]); $this->pdf->SetFont('hack', 'B', 8); } $this->pdf->Cell($colWidths[$i], $rowHeight, $cells[$i], 1, 0, $align, true); if ($i === 2) { $this->pdf->SetTextColor(0, 0, 0); $this->pdf->SetFont('hack', '', 8); } $x += $colWidths[$i]; } $y += $rowHeight; // Seitenumbruch mit erneuter Kopfzeile if ($y > $this->pageHeight - 30) { $this->drawTitleBlock(0, 0); $this->pdf->AddPage($this->orientation, array($this->pageWidth, $this->pageHeight)); $y = self::MARGIN_TOP; // Kopfzeile wiederholen $this->pdf->SetFont('hack', 'B', 8); $this->pdf->SetFillColor(60, 60, 60); $this->pdf->SetTextColor(255, 255, 255); $x = self::MARGIN_LEFT; for ($i = 0; $i < count($colHeaders); $i++) { $this->pdf->SetXY($x, $y); $this->pdf->Cell($colWidths[$i], $headerHeight, $colHeaders[$i], 1, 0, 'C', true); $x += $colWidths[$i]; } $y += $headerHeight; $this->pdf->SetTextColor(0, 0, 0); $this->pdf->SetFont('hack', '', 8); } } // Titelfeld $this->drawTitleBlock(0, 0); } /** * Legende zeichnen */ public function renderLegende() { $this->pdf->AddPage($this->orientation, array($this->pageWidth, $this->pageHeight)); $y = self::MARGIN_TOP; $x = self::MARGIN_LEFT; $this->pdf->SetFont('hack', 'B', 14); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->Text($x, $y + 5, 'Legende'); $y += 15; // Phasenfarben $this->pdf->SetFont('hack', 'B', 10); $this->pdf->Text($x, $y, 'Phasenfarben nach DIN VDE'); $y += 8; $phases = array( 'L1' => 'Außenleiter 1 (Braun)', 'L2' => 'Außenleiter 2 (Schwarz)', 'L3' => 'Außenleiter 3 (Grau)', 'N' => 'Neutralleiter (Blau)', 'PE' => 'Schutzleiter (Grün-Gelb)', ); $this->pdf->SetFont('hack', '', 8); foreach ($phases as $phase => $label) { $rgb = getPhaseColorRGB($phase); $this->pdf->SetFillColor($rgb[0], $rgb[1], $rgb[2]); $this->pdf->Rect($x, $y, 20, 5, 'F'); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->Text($x + 25, $y + 3, $phase.' - '.$label); $y += 8; } $y += 5; // Symbole $this->pdf->SetFont('hack', 'B', 10); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->Text($x, $y, 'Symbole'); $y += 10; // Hauptschalter-Symbol (3-polig mit mechanischer Kopplung) $this->pdf->SetDrawColor(0, 0, 0); $this->pdf->SetLineWidth(0.3); $hsSymX = $x + 5; $hsSymY = $y + 2; $hsSpacing = 5; for ($p = 0; $p < 3; $p++) { $px = $hsSymX + $p * $hsSpacing; $this->pdf->Circle($px, $hsSymY, 0.7, 0, 360, 'F', array(), array(0, 0, 0)); $this->pdf->Line($px, $hsSymY, $px + 2, $hsSymY + 5); $this->pdf->Circle($px, $hsSymY + 6, 0.7, 0, 360, 'F', array(), array(0, 0, 0)); } // Mechanische Kopplung $this->pdf->SetLineStyle(array('width' => 0.2, 'dash' => '1,1', 'color' => array(0, 0, 0))); $this->pdf->Line($hsSymX + 1, $hsSymY + 2, $hsSymX + 2 * $hsSpacing + 1, $hsSymY + 2); $this->pdf->SetLineStyle(array('dash' => 0)); $this->pdf->SetFont('hack', '', 8); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->Text($x + 30, $y + 5, 'Hauptschalter 3-polig (Q0)'); $y += 14; // LS-Symbol + Beschreibung $this->pdf->SetFont('hack', '', 8); $this->drawBreakerSymbol($x + 8, $y, (object)array('label'=>'F1'), array(array('type'=>'breaker','block_label'=>'B16'))); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->Text($x + 30, $y + 8, 'Leitungsschutzschalter (LS-Schalter)'); $y += 25; // FI-Symbol + Beschreibung $protDummy = new stdClass(); $protDummy->label = 'Q1'; $protDummy->type_label_short = 'FI'; // getBlockLabel brauchen wir nicht, Text wird direkt gesetzt $this->pdf->SetDrawColor(0, 0, 0); $this->pdf->SetLineWidth(0.3); $this->pdf->SetFillColor(255, 255, 255); $this->pdf->Rect($x, $y, 16, 20, 'DF'); $this->pdf->Circle($x + 8, $y + 10, 4, 0, 360, 'D'); $this->pdf->Line($x + 8, $y + 6, $x + 8, $y + 14); $this->pdf->SetFont('hack', '', 5); $this->pdf->Text($x + 2, $y + 17, 'FI/RCD'); $this->pdf->SetFont('hack', '', 8); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->Text($x + 30, $y + 10, 'Fehlerstrom-Schutzschalter (FI/RCD)'); $y += 28; // Abgang-Pfeil $this->pdf->SetFillColor(0, 0, 0); $this->pdf->Polygon(array($x + 5, $y, $x + 11, $y, $x + 8, $y + 5), 'F'); $this->pdf->SetFont('hack', '', 8); $this->pdf->Text($x + 30, $y + 3, 'Abgang zum Verbraucher'); $y += 15; // Norm-Hinweis $y += 10; $this->pdf->SetFont('hack', 'I', 7); $this->pdf->SetTextColor(120, 120, 120); $this->pdf->Text($x, $y, 'Erstellt nach DIN EN 61082 / DIN EN 81346'); $this->pdf->Text($x, $y + 5, 'Bezugsbezeichnungen nach DIN EN 81346-2'); $this->pdf->Text($x, $y + 10, 'Schaltzeichen nach DIN EN 60617'); $this->drawTitleBlock(0, 0); } /** * Titelfeld nach DIN EN 61082 / ISO 7200 */ private function drawTitleBlock($pageNum = 0, $totalPages = 0) { $titleBlockWidth = 180; $titleBlockHeight = 56; $titleBlockX = $this->pageWidth - $titleBlockWidth - 10; $titleBlockY = $this->pageHeight - $titleBlockHeight - 10; // Rahmen $this->pdf->SetDrawColor(0, 0, 0); $this->pdf->SetLineWidth(0.5); $this->pdf->Rect($titleBlockX, $titleBlockY, $titleBlockWidth, $titleBlockHeight); $rowHeight = 8; $col1 = 30; $col2 = 50; $col3 = 50; $col4 = 50; // Horizontale Linien (nur bis Zeile 5, Zeile 6-7 = Firmenname ohne Trennung) for ($i = 1; $i <= 5; $i++) { $y = $titleBlockY + ($i * $rowHeight); $this->pdf->Line($titleBlockX, $y, $titleBlockX + $titleBlockWidth, $y); } // Vertikale Linien (nur bis Zeile 5, Firmenname spannt volle Breite) $verticalBottom = $titleBlockY + (5 * $rowHeight); $this->pdf->Line($titleBlockX + $col1, $titleBlockY, $titleBlockX + $col1, $verticalBottom); $this->pdf->Line($titleBlockX + $col1 + $col2, $titleBlockY, $titleBlockX + $col1 + $col2, $verticalBottom); $this->pdf->Line($titleBlockX + $col1 + $col2 + $col3, $titleBlockY, $titleBlockX + $col1 + $col2 + $col3, $verticalBottom); $this->pdf->SetTextColor(0, 0, 0); // Zeile 1: Titel $this->pdf->SetFont('hack', 'B', 12); $this->pdf->SetXY($titleBlockX + 2, $titleBlockY + 1); $this->pdf->Cell($titleBlockWidth - 4, $rowHeight - 2, 'LEITUNGSLAUFPLAN', 0, 0, 'C'); // Zeile 2: Anlage $this->pdf->SetFont('hack', 'B', 10); $this->pdf->SetXY($titleBlockX + 2, $titleBlockY + $rowHeight + 1); $this->pdf->Cell($titleBlockWidth - 4, $rowHeight - 2, $this->anlage->label, 0, 0, 'C'); // Zeile 3: Erstellt | Kunde | Projekt | Blatt $y = $titleBlockY + (2 * $rowHeight); $this->pdf->SetFont('hack', '', 6); $this->pdf->SetXY($titleBlockX + 1, $y + 1); $this->pdf->Cell($col1 - 2, 3, 'Erstellt', 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + 1, $y + 1); $this->pdf->Cell($col2 - 2, 3, 'Kunde', 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 1); $this->pdf->Cell($col3 - 2, 3, 'Projekt-Nr.', 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 1); $this->pdf->Cell($col4 - 2, 3, 'Blatt', 0, 0); $this->pdf->SetFont('hack', '', 8); $this->pdf->SetXY($titleBlockX + 1, $y + 4); $this->pdf->Cell($col1 - 2, 4, dol_print_date(dol_now(), 'day'), 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + 1, $y + 4); $this->pdf->Cell($col2 - 2, 4, dol_trunc($this->societe->name, 25), 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 4); $this->pdf->Cell($col3 - 2, 4, $this->anlage->ref ?: '-', 0, 0); $blatt = ($pageNum > 0 && $totalPages > 0) ? $pageNum.' / '.$totalPages : ''; $this->pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 4); $this->pdf->Cell($col4 - 2, 4, $blatt, 0, 0); // Zeile 4: Bearbeiter | Adresse | Anlage | Revision $y = $titleBlockY + (3 * $rowHeight); $this->pdf->SetFont('hack', '', 6); $this->pdf->SetXY($titleBlockX + 1, $y + 1); $this->pdf->Cell($col1 - 2, 3, 'Bearbeiter', 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + 1, $y + 1); $this->pdf->Cell($col2 - 2, 3, 'Adresse', 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 1); $this->pdf->Cell($col3 - 2, 3, 'Anlage', 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 1); $this->pdf->Cell($col4 - 2, 3, 'Revision', 0, 0); $this->pdf->SetFont('hack', '', 8); global $langs; $this->pdf->SetXY($titleBlockX + 1, $y + 4); $this->pdf->Cell($col1 - 2, 4, dol_trunc($this->user->getFullName($langs), 15), 0, 0); $address = trim(($this->societe->address ?? '').' '.($this->societe->zip ?? '').' '.($this->societe->town ?? '')); $this->pdf->SetXY($titleBlockX + $col1 + 1, $y + 4); $this->pdf->Cell($col2 - 2, 4, dol_trunc($address, 25), 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 4); $this->pdf->Cell($col3 - 2, 4, $this->anlage->type_label ?? '-', 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 4); $this->pdf->Cell($col4 - 2, 4, 'A', 0, 0); // Zeile 5: Abgänge | Format | Norm $y = $titleBlockY + (4 * $rowHeight); $this->pdf->SetFont('hack', '', 6); $this->pdf->SetXY($titleBlockX + 1, $y + 1); $this->pdf->Cell($col1 - 2, 3, 'Abgänge', 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + 1, $y + 1); $this->pdf->Cell($col2 - 2, 3, 'Format', 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 1); $this->pdf->Cell($col3 + $col4 - 2, 3, 'Norm', 0, 0); $this->pdf->SetFont('hack', '', 8); $this->pdf->SetXY($titleBlockX + 1, $y + 4); $this->pdf->Cell($col1 - 2, 4, count($this->circuitPaths), 0, 0); $this->pdf->SetXY($titleBlockX + $col1 + 1, $y + 4); $this->pdf->Cell($col2 - 2, 4, $this->format.' '.$this->orientation, 0, 0); $this->pdf->SetFont('hack', '', 6); $this->pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 4); $this->pdf->Cell($col3 + $col4 - 2, 4, 'DIN EN 61082 / DIN EN 81346', 0, 0); // Zeile 6-7: Firmenname $y = $titleBlockY + (5 * $rowHeight); $this->pdf->SetFont('hack', 'B', 9); $this->pdf->SetXY($titleBlockX + 2, $y + 3); $this->pdf->Cell($titleBlockWidth - 4, $rowHeight * 2 - 6, $GLOBALS['mysoc']->name ?? 'ALLES WATT LÄUFT', 0, 0, 'C'); } // ====================================================================== // Verteilungs-Tabellen (A4 Hochformat) // ====================================================================== /** * Gemeinsamer Seiten-Header für Verteilungs-Tabellen * Titel, Kundendaten, Firmenlogo * * @param string $title Seitentitel (z.B. "Verteilung") * @return float Y-Position nach dem Header */ private function drawVerteilungHeader($title) { $margin = 20; // Titel $this->pdf->SetFont('hack', 'B', 16); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->SetXY($margin, 15); $this->pdf->Cell(170, 10, $title, 0, 0, 'C'); // Unterstrich $this->pdf->SetLineWidth(0.5); $this->pdf->SetDrawColor(0, 0, 0); $titleWidth = $this->pdf->GetStringWidth($title); $titleCenter = $margin + 85; $this->pdf->Line($titleCenter - $titleWidth / 2, 26, $titleCenter + $titleWidth / 2, 26); // Felder links $this->pdf->SetFont('hack', 'B', 9); $labelX = $margin + 5; $valueX = $margin + 30; $y = 32; $fields = array( 'Name:' => $this->societe->name ?? '', 'Objekt:' => trim(($this->societe->address ?? '').' '.($this->societe->zip ?? '').' '.($this->societe->town ?? '')), 'Raum:' => '', 'Verteilung:' => $this->anlage->label ?? '', ); foreach ($fields as $label => $value) { $this->pdf->SetFont('hack', 'B', 9); $this->pdf->Text($labelX, $y, $label); $this->pdf->SetFont('hack', '', 9); $this->pdf->Text($valueX, $y, dol_trunc($value, 50)); $y += 5; } // Firmenname rechts oben $this->pdf->SetFont('hack', 'B', 10); $this->pdf->SetXY(140, 32); $this->pdf->Cell(50, 5, $GLOBALS['mysoc']->name ?? 'ALLES WATT LÄUFT', 0, 0, 'R'); $this->pdf->SetFont('hack', '', 7); $this->pdf->SetTextColor(100, 100, 100); $this->pdf->SetXY(140, 38); $fullName = $this->user->getFullName($GLOBALS['langs']); $this->pdf->Cell(50, 4, 'Inh. '.$fullName, 0, 0, 'R'); $this->pdf->SetTextColor(0, 0, 0); // Trennlinie $y = 56; $this->pdf->SetLineWidth(0.3); $this->pdf->Line($margin, $y, 210 - $margin, $y); return $y + 8; } /** * Tabellen-Sektion zeichnen: "Feld X - Reihe Y" Header + Tabelle * * @param float $y Aktuelle Y-Position * @param string $sectionTitle z.B. "Feld 2 - Reihe 3" * @param array $colHeaders Spalten-Überschriften * @param array $colWidths Spaltenbreiten * @param array $rows Datenzeilen (je ein Array mit Werten in gleicher Reihenfolge wie colHeaders) * @param string $pageTitle Für Header-Wiederholung bei Seitenumbruch * @return float Neue Y-Position nach der Tabelle */ private function drawVerteilungTable(&$y, $sectionTitle, $colHeaders, $colWidths, $rows, $pageTitle) { $margin = 20; $headerHeight = 7; $rowHeight = 6.5; $totalWidth = array_sum($colWidths); $pageBottom = 277; // A4: 297 - 20mm Rand // Prüfen ob genug Platz für Header + mindestens 2 Zeilen if ($y + 20 + $headerHeight + $rowHeight * 2 > $pageBottom) { $this->pdf->AddPage('P', array(210, 297)); $y = $this->drawVerteilungHeader($pageTitle); } // Sektions-Header (z.B. "Feld 2 - Reihe 3") $this->pdf->SetFont('hack', 'B', 9); $this->pdf->SetTextColor(0, 0, 0); $this->pdf->SetFillColor(230, 230, 230); $this->pdf->SetXY($margin, $y); $this->pdf->Cell($totalWidth, $headerHeight, ' '.$sectionTitle, 1, 0, 'L', true); $y += $headerHeight; // Spalten-Header $this->pdf->SetFont('hack', 'B', 8); $this->pdf->SetFillColor(240, 240, 240); $this->pdf->SetTextColor(0, 0, 0); $x = $margin; for ($i = 0; $i < count($colHeaders); $i++) { $this->pdf->SetXY($x, $y); $this->pdf->Cell($colWidths[$i], $headerHeight, $colHeaders[$i], 1, 0, 'C', true); $x += $colWidths[$i]; } $y += $headerHeight; // Datenzeilen $this->pdf->SetFont('hack', '', 8); foreach ($rows as $rowIdx => $cells) { // Seitenumbruch? if ($y + $rowHeight > $pageBottom) { $this->drawVerteilungFooter(); $this->pdf->AddPage('P', array(210, 297)); $y = $this->drawVerteilungHeader($pageTitle); // Sektions-Header wiederholen $this->pdf->SetFont('hack', 'B', 9); $this->pdf->SetFillColor(230, 230, 230); $this->pdf->SetXY($margin, $y); $this->pdf->Cell($totalWidth, $headerHeight, ' '.$sectionTitle.' (Forts.)', 1, 0, 'L', true); $y += $headerHeight; // Spalten-Header wiederholen $this->pdf->SetFont('hack', 'B', 8); $this->pdf->SetFillColor(240, 240, 240); $x = $margin; for ($i = 0; $i < count($colHeaders); $i++) { $this->pdf->SetXY($x, $y); $this->pdf->Cell($colWidths[$i], $headerHeight, $colHeaders[$i], 1, 0, 'C', true); $x += $colWidths[$i]; } $y += $headerHeight; $this->pdf->SetFont('hack', '', 8); } // Zeile zeichnen $x = $margin; for ($i = 0; $i < count($cells); $i++) { $this->pdf->SetXY($x, $y); $this->pdf->Cell($colWidths[$i], $rowHeight, ' '.$cells[$i], 1, 0, 'L'); $x += $colWidths[$i]; } $y += $rowHeight; } $y += 5; // Abstand zwischen Sektionen return $y; } /** * Footer für Verteilungs-Seiten (Seitenzahl) */ private function drawVerteilungFooter() { // Wird am Ende pro Seite gesetzt (aliasNbPages) } /** * Kundenansicht rendern (A4 Hochformat) * Einfache Tabelle: Nr. | Verbraucher | Räumlichkeit * Gruppiert nach Feld (Panel) und Reihe (Carrier) */ public function renderKundenansicht() { $grouped = $this->analyzer->getVerteilungData(); if (empty($grouped)) return; $pageTitle = 'Verteilung'; // Erste Seite $this->pdf->AddPage('P', array(210, 297)); $y = $this->drawVerteilungHeader($pageTitle); // Spalten-Definition $totalWidth = 170; // 210 - 2*20mm Rand $colHeaders = array('Nr.', 'Verbraucher', 'Räumlichkeit'); $colWidths = array( round($totalWidth * 0.12), // Nr. round($totalWidth * 0.50), // Verbraucher ); $colWidths[] = $totalWidth - array_sum($colWidths); // Räumlichkeit = Rest foreach ($grouped as $panelId => $pData) { foreach ($pData['carriers'] as $carrierId => $cData) { $panel = $pData['panel']; $carrier = $cData['carrier']; // Sektions-Titel: "Feld X - Reihe Y" $panelLabel = $panel ? $panel->label : 'Feld ?'; $carrierLabel = $carrier ? $carrier->label : 'Reihe ?'; $sectionTitle = $panelLabel.' - '.$carrierLabel; // Zeilen aufbauen $rows = array(); foreach ($cData['paths'] as $path) { $rows[] = array( $path['abgang_nr'], $path['output_label'], $path['output_location'], ); } $y = $this->drawVerteilungTable($y, $sectionTitle, $colHeaders, $colWidths, $rows, $pageTitle); } } } /** * Technikeransicht rendern (A4 Hochformat) * Erweiterte Tabelle: R.Klem. | FI | Nr. | Verbraucher | Räumlichkeit | Typ * Gruppiert nach Feld (Panel) und Reihe (Carrier) */ public function renderTechnikeransicht() { $grouped = $this->analyzer->getVerteilungData(); if (empty($grouped)) return; $pageTitle = 'Verteilung'; // Erste Seite $this->pdf->AddPage('P', array(210, 297)); $y = $this->drawVerteilungHeader($pageTitle); // Spalten-Definition $totalWidth = 170; // 210 - 2*20mm Rand $colHeaders = array('R.Klem.', 'FI', 'Nr.', 'Verbraucher', 'Räumlichkeit', 'Typ'); $colWidths = array( round($totalWidth * 0.10), // R.Klem. round($totalWidth * 0.08), // FI round($totalWidth * 0.10), // Nr. round($totalWidth * 0.30), // Verbraucher round($totalWidth * 0.25), // Räumlichkeit ); $colWidths[] = $totalWidth - array_sum($colWidths); // Typ = Rest foreach ($grouped as $panelId => $pData) { foreach ($pData['carriers'] as $carrierId => $cData) { $panel = $pData['panel']; $carrier = $cData['carrier']; // Sektions-Titel: "Feld X - Reihe Y" $panelLabel = $panel ? $panel->label : 'Feld ?'; $carrierLabel = $carrier ? $carrier->label : 'Reihe ?'; $sectionTitle = $panelLabel.' - '.$carrierLabel; // Zeilen aufbauen $rows = array(); foreach ($cData['paths'] as $path) { // FI/RCD Label $fiLabel = ''; if ($path['protection_device']) { $pd = $path['protection_device']; $fiLabel = $pd->label ?: ($pd->type_label_short ?: 'FI'); } // Kabel-Typ (Querschnitt) $kabelTyp = ''; if ($path['medium_spec']) $kabelTyp = $path['medium_spec']; elseif ($path['medium_type']) $kabelTyp = $path['medium_type']; $rows[] = array( '', // R.Klem. — leer (Reihenklemmen noch nicht implementiert) $fiLabel, $path['abgang_nr'], $path['output_label'], $path['output_location'], $kabelTyp, ); } $y = $this->drawVerteilungTable($y, $sectionTitle, $colHeaders, $colWidths, $rows, $pageTitle); } } } }