Source code for districtheatingsim.gui.dialogs

"""GUI Dialogs Module
==================

Dialog windows for user input and configuration.

:author: Dipl.-Ing. (FH) Jonas Pfeiffer
"""

import os
import re

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QDialog, QFileDialog, QHBoxLayout, QLabel, QLineEdit, QPushButton, QVBoxLayout

from districtheatingsim.utilities.utilities import get_resource_path


def _extract_try_location(path: str) -> str:
    """
    Extract the geographic location from a TRY file path.

    Tries two methods in order:
    1. Filename pattern ``TRY????_LLLLLLWWWWWW_*.dat`` (lat = LLLLLL/10000, lon = WWWWWW/10000)
    2. File header ``Rechtswert`` / ``Hochwert`` lines → convert EPSG:3034 (LCC) → WGS84

    :param path: Path to the TRY .dat file
    :type path: str
    :return: Human-readable location string, e.g. ``"51.1676 °N, 14.4222 °E"``
    :rtype: str
    """
    # 1. Filename-based extraction
    m = re.search(r"_(\d{6})(\d{6})_", os.path.basename(path))
    if m:
        lat = int(m.group(1)) / 10000
        lon = int(m.group(2)) / 10000
        return f"{lat:.4f} °N, {lon:.4f} °E"

    # 2. Header-based extraction (Lambert Conformal Conic → WGS84)
    try:
        rw = hw = None
        with open(path, encoding="latin-1", errors="replace") as f:
            for line in f:
                if line.startswith("Rechtswert"):
                    rw = float(line.split(":")[1].split()[0])
                elif line.startswith("Hochwert"):
                    hw = float(line.split(":")[1].split()[0])
                if rw is not None and hw is not None:
                    break
        if rw is not None and hw is not None:
            from pyproj import Transformer

            t = Transformer.from_crs("EPSG:3034", "EPSG:4326", always_xy=True)
            lon, lat = t.transform(rw, hw)
            return f"{lat:.4f} °N, {lon:.4f} °E"
    except Exception:
        pass

    return ""


[docs] class TemperatureDataDialog(QDialog): """ Dialog for selecting TRY (Test Reference Year) weather data files. .. note:: Simple file selection dialog for weather data input with default path. """
[docs] def __init__(self, parent=None): """ Initialize temperature data dialog. :param parent: Parent widget, defaults to None :type parent: QWidget, optional """ super().__init__(parent) self.initUI()
[docs] def initUI(self): """ Initialize the user interface components with file selection controls. .. note:: Creates description label with DWD link, file input field with default TRY path, and OK/Cancel buttons. """ self.setWindowTitle("Testreferenzjahr-Datei auswählen") self.resize(600, 200) self.main_layout = QVBoxLayout(self) # Description label with DWD link self.descriptionLabel = QLabel( "Bitte wählen Sie die Datei des Testreferenzjahres (TRY) aus, die Sie verwenden möchten.<br>" "Testreferenzjahre können unter <a href='https://www.dwd.de/DE/leistungen/testreferenzjahre/testreferenzjahre.html'>" "https://www.dwd.de/DE/leistungen/testreferenzjahre/testreferenzjahre.html</a> bezogen werden.<br>" "Es ist jedoch eine Registrierung beim DWD notwendig", self, ) self.descriptionLabel.setWordWrap(True) self.descriptionLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) self.main_layout.addWidget(self.descriptionLabel) # File input controls self.input_layout = QHBoxLayout() self.temperatureDataFileLabel = QLabel("TRY-Datei:", self) self.temperatureDataFileInput = QLineEdit(self) self.temperatureDataFileInput.setText( get_resource_path(os.path.join("data", "TRY", "TRY_511676144222", "TRY2015_511676144222_Jahr.dat")) ) self.selectTRYFileButton = QPushButton("TRY-Datei auswählen") self.selectTRYFileButton.clicked.connect(lambda: self.selectFilename(self.temperatureDataFileInput)) self.input_layout.addWidget(self.temperatureDataFileLabel) self.input_layout.addWidget(self.temperatureDataFileInput) self.input_layout.addWidget(self.selectTRYFileButton) self.main_layout.addLayout(self.input_layout) # Location label (updated whenever the file path changes) self.locationLabel = QLabel("", self) self.locationLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) self.main_layout.addWidget(self.locationLabel) self.temperatureDataFileInput.textChanged.connect(self._update_location_label) self._update_location_label(self.temperatureDataFileInput.text()) # OK/Cancel buttons self.buttonLayout = QHBoxLayout() okButton = QPushButton("OK", self) cancelButton = QPushButton("Abbrechen", self) okButton.clicked.connect(self.accept) cancelButton.clicked.connect(self.reject) self.buttonLayout.addWidget(okButton) self.buttonLayout.addWidget(cancelButton) self.main_layout.addLayout(self.buttonLayout)
def _update_location_label(self, path: str): """Update the location label from the given TRY file path.""" loc = _extract_try_location(path) if path else "" if loc: self.locationLabel.setText(f"Standort: {loc}") else: self.locationLabel.setText("")
[docs] def selectFilename(self, lineEdit): """ Open file dialog and set selected path to line edit. :param lineEdit: Target line edit widget :type lineEdit: QLineEdit """ filename, _ = QFileDialog.getOpenFileName(self, "Datei auswählen") if filename: lineEdit.setText(filename)
[docs] def getValues(self): """ Get dialog values. :return: Dictionary with TRY filename :rtype: dict """ return {"TRY-filename": self.temperatureDataFileInput.text()}
[docs] class HeatPumpDataDialog(QDialog): """ Dialog for selecting heat pump COP data files with inline Kennfeld visualisation. The dialog embeds a matplotlib heatmap that updates whenever a new CSV is selected, so the user can immediately judge whether the data looks plausible. CSV format expected: first row = supply temperatures (header), first column = source/ambient temperatures, matrix values = COP. Zero entries are treated as not feasible and rendered distinctly (white/grey). """
[docs] def __init__(self, parent=None): super().__init__(parent) self.initUI()
[docs] def initUI(self): self.setWindowTitle("COP-Daten-Verwaltung") self.resize(620, 560) mainLayout = QVBoxLayout(self) # File input row fileRow = QHBoxLayout() self.heatPumpDataFileLabel = QLabel("CSV-Datei mit Wärmepumpenkennfeld:") self.heatPumpDataFileInput = QLineEdit() self.heatPumpDataFileInput.setText(get_resource_path("data/COP/Kennlinien WP.csv")) self.selectCOPFileButton = QPushButton("CSV-Datei auswählen") self.selectCOPFileButton.clicked.connect(lambda: self.selectFilename(self.heatPumpDataFileInput)) fileRow.addWidget(self.heatPumpDataFileLabel) fileRow.addWidget(self.heatPumpDataFileInput) fileRow.addWidget(self.selectCOPFileButton) mainLayout.addLayout(fileRow) # Kennfeld canvas from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg from matplotlib.figure import Figure self._fig = Figure(figsize=(5.5, 3.8), tight_layout=True) self._ax = self._fig.add_subplot(111) self._canvas = FigureCanvasQTAgg(self._fig) self._colorbar = None mainLayout.addWidget(self._canvas) # OK/Cancel buttons buttonLayout = QHBoxLayout() okButton = QPushButton("OK") cancelButton = QPushButton("Abbrechen") okButton.clicked.connect(self.accept) cancelButton.clicked.connect(self.reject) buttonLayout.addWidget(okButton) buttonLayout.addWidget(cancelButton) mainLayout.addLayout(buttonLayout) self.setLayout(mainLayout) # Wire up live preview self.heatPumpDataFileInput.textChanged.connect(self._update_kennfeld) self._update_kennfeld(self.heatPumpDataFileInput.text())
# ------------------------------------------------------------------ def _update_kennfeld(self, path: str): """Re-draw the COP heatmap for *path*. Silently clears on any error.""" import numpy as np self._ax.clear() try: import pandas as pd df = pd.read_csv(path, sep=";", header=0, index_col=0) df = df.apply(pd.to_numeric, errors="coerce").fillna(0) data = df.values.astype(float) supply_temps = [str(c) for c in df.columns] source_temps = [str(i) for i in df.index] # Mask zero / infeasible cells masked = np.ma.masked_where(data == 0, data) im = self._ax.imshow( masked, aspect="auto", origin="lower", cmap="RdYlGn", vmin=1.0, vmax=7.0, ) # Annotate each cell for r in range(data.shape[0]): for c in range(data.shape[1]): val = data[r, c] if val > 0: self._ax.text(c, r, f"{val:.1f}", ha="center", va="center", fontsize=7, color="black") self._ax.set_xticks(range(len(supply_temps))) self._ax.set_xticklabels(supply_temps, fontsize=8) self._ax.set_yticks(range(len(source_temps))) self._ax.set_yticklabels(source_temps, fontsize=8) self._ax.set_xlabel("Vorlauftemperatur [°C]", fontsize=9) self._ax.set_ylabel("Quelltemperatur [°C]", fontsize=9) self._ax.set_title("COP-Kennfeld (weiß = nicht realisierbar)", fontsize=9) if self._colorbar is None: self._colorbar = self._fig.colorbar(im, ax=self._ax, label="COP") else: self._colorbar.update_normal(im) self._ax.set_visible(True) except Exception: self._ax.set_visible(False) if self._colorbar is not None: self._colorbar.remove() self._colorbar = None self._canvas.draw_idle() # ------------------------------------------------------------------
[docs] def selectFilename(self, lineEdit): """Open file dialog and set selected path to line edit.""" filename, _ = QFileDialog.getOpenFileName(self, "Datei auswählen", "", "CSV-Dateien (*.csv)") if filename: lineEdit.setText(filename)
[docs] def getValues(self): """ Get dialog values. :return: Dictionary with COP filename :rtype: dict """ return {"COP-filename": self.heatPumpDataFileInput.text()}