kundenkarte/lib/wiring_diagram.lib.php
data 16e51a799a feat(v8.6): Räumlichkeit, Verteilungs-Tabellen, Bundled-Terminals, PWA-Updates
- output_location (Räumlichkeit): Neues Textfeld am Abgang für Raum/Ort des
  Verbrauchers. DB-Migration, Backend (AJAX), Frontend (Website + PWA),
  Anzeige im Schaltplan (kursiv) und in PDF-Tabellen.
- Verteilungs-Tabellen: Kundenansicht (A4, Nr/Verbraucher/Räumlichkeit) und
  Technikeransicht (A4, R.Klem/FI/Nr/Verbraucher/Räumlichkeit/Typ) im
  Leitungslaufplan-PDF. Gruppiert nach Feld/Reihe mit automatischem Seitenumbruch.
- Bundled-Terminals Checkbox: Im Website-Abgang-Dialog (war vorher nur PWA).
- PWA: Diverse Verbesserungen, Service Worker v12.4, Connection-Modal erweitert.
- Typ-Flags: has_product auch für Gebäudetypen, Equipment-Typ Erweiterungen.
- CLAUDE.md + Doku aktualisiert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 01:33:05 +01:00

2131 lines
70 KiB
PHP

<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* Leitungslaufplan (Stromlaufplan in aufgelöster Darstellung)
* PDF-Export nach DIN EN 61082
*
* Dieses Feature ist vollständig separiert und kann ohne Auswirkungen
* auf den restlichen Code entfernt werden.
*/
dol_include_once('/kundenkarte/class/anlage.class.php');
dol_include_once('/kundenkarte/class/equipment.class.php');
dol_include_once('/kundenkarte/class/equipmentpanel.class.php');
dol_include_once('/kundenkarte/class/equipmentcarrier.class.php');
dol_include_once('/kundenkarte/class/equipmentconnection.class.php');
/**
* Phasenfarben nach DIN VDE (RGB-Arrays für TCPDF)
*/
function getPhaseColorRGB($phase)
{
$colors = array(
'L1' => 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);
}
}
}
}