Source code for districtheatingsim.gui.ProjectTab.project_tab_dialogs

"""
Project Tab Dialogs Module
==========================

Dialog windows for project tab functionality including data input and progress display.

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

from geopy.geocoders import Nominatim
from pyproj import Transformer
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QCheckBox,
    QDialog,
    QDialogButtonBox,
    QFrame,
    QGridLayout,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QProgressBar,
    QPushButton,
    QScrollArea,
    QTableWidget,
    QTableWidgetItem,
    QVBoxLayout,
)


[docs] class RowInputDialog(QDialog): """ Dialog for adding new table rows with input fields. """
[docs] def __init__(self, headers, parent=None): """ Initialize row input dialog. :param headers: Column headers for input fields. :type headers: list :param parent: Parent widget. :type parent: QWidget """ super().__init__(parent) self.setWindowTitle("Neue Zeile hinzufügen") self.layout = QGridLayout(self) self.fields = {} # Create input fields with labels for i, header in enumerate(headers): label = QLabel(header) lineEdit = QLineEdit() lineEdit.setPlaceholderText(f"Geben Sie {header} ein") self.layout.addWidget(label, i, 0) self.layout.addWidget(lineEdit, i, 1) self.fields[header] = lineEdit # Dialog buttons buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) buttonBox.accepted.connect(self.accept) buttonBox.rejected.connect(self.reject) self.layout.addWidget(buttonBox, len(headers), 0, 1, 2)
[docs] def get_input_data(self): """ Get input data from dialog fields. :return: Mapping of headers to input values. :rtype: dict """ return {header: field.text() for header, field in self.fields.items()}
[docs] class OSMImportDialog(QDialog): """ Dialog for OSM data import with default building parameters. """
[docs] def __init__(self, parent=None, sample_utm_coords=None, project_crs: str = "EPSG:25833"): """ Initialize OSM import dialog. :param parent: Parent widget. :type parent: QWidget :param sample_utm_coords: Sample (UTM_X, UTM_Y) coordinates for reverse geocoding. :type sample_utm_coords: tuple :param project_crs: Projected CRS of the sample coordinates. :type project_crs: str """ super().__init__(parent) self.setWindowTitle("OSM-Daten importieren") self.layout = QGridLayout(self) self.fields = {} self.sample_utm_coords = sample_utm_coords self.project_crs = project_crs # Define the default values for the fields self.default_values = { "Land": "Deutschland", "Bundesland": "", "Stadt": "", "Adresse": "", "Wärmebedarf": "30000", "Gebäudetyp": "HMF", "Subtyp": "05", "WW_Anteil": "0.2", "Typ_Heizflächen": "HK", "VLT_max": "70", "Steigung_Heizkurve": "1.5", "RLT_max": "55", "Normaußentemperatur": "-15", } # Add checkbox for auto-fill if sample_utm_coords: self.auto_fill_checkbox = QCheckBox("Adressdaten automatisch aus Koordinaten ermitteln") self.auto_fill_checkbox.setChecked(True) self.auto_fill_checkbox.stateChanged.connect(self._on_auto_fill_changed) self.layout.addWidget(self.auto_fill_checkbox, 0, 0, 1, 2) row_offset = 1 else: row_offset = 0 # Create input fields with labels and default values for i, (header, value) in enumerate(self.default_values.items()): label = QLabel(header) lineEdit = QLineEdit(value) lineEdit.setPlaceholderText(f"Geben Sie {header} ein") self.layout.addWidget(label, i + row_offset, 0) self.layout.addWidget(lineEdit, i + row_offset, 1) self.fields[header] = lineEdit # Auto-fill address fields if coordinates provided if sample_utm_coords and self.auto_fill_checkbox.isChecked(): self._reverse_geocode_and_fill() # Dialog buttons buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) buttonBox.accepted.connect(self.accept) buttonBox.rejected.connect(self.reject) self.layout.addWidget(buttonBox, len(self.default_values) + row_offset, 0, 1, 2)
def _on_auto_fill_changed(self, state): """ Handle auto-fill checkbox state change. :param state: Checkbox state. :type state: int """ if state == Qt.CheckState.Checked.value and self.sample_utm_coords: self._reverse_geocode_and_fill() elif state == Qt.CheckState.Unchecked.value: # Clear address fields self.fields["Land"].setText("Deutschland") self.fields["Bundesland"].setText("") self.fields["Stadt"].setText("") self.fields["Adresse"].setText("") def _reverse_geocode_and_fill(self): """ Reverse geocode UTM coordinates and fill address fields. """ if not self.sample_utm_coords: return try: utm_x, utm_y = self.sample_utm_coords # Transform project CRS to WGS84 transformer = Transformer.from_crs(self.project_crs, "epsg:4326", always_xy=True) lon, lat = transformer.transform(utm_x, utm_y) # Reverse geocode geolocator = Nominatim(user_agent="DistrictHeatingSim") location = geolocator.reverse(f"{lat}, {lon}", language="de") if location and location.raw.get("address"): address = location.raw["address"] # Fill fields from geocoding result if "country" in address: self.fields["Land"].setText(address["country"]) if "state" in address: self.fields["Bundesland"].setText(address["state"]) # Try different keys for city city = ( address.get("city") or address.get("town") or address.get("village") or address.get("municipality") or "" ) if city: self.fields["Stadt"].setText(city) # Build street address street_parts = [] if "road" in address: street_parts.append(address["road"]) if "house_number" in address: street_parts.append(address["house_number"]) if street_parts: self.fields["Adresse"].setText(" ".join(street_parts)) except Exception: pass
[docs] def get_input_data(self): """ Get OSM import parameters. :return: Mapping of parameters to input values. :rtype: dict """ return {header: field.text() for header, field in self.fields.items()}
[docs] class ProcessDetailsDialog(QDialog): """ Dialog displaying detailed project progress information. """
[docs] def __init__(self, process_steps, parent=None): """ Initialize process details dialog. :param process_steps: List of process step dictionaries with progress info. :type process_steps: list :param parent: Parent widget. :type parent: QWidget """ super().__init__(parent) self.setWindowTitle("Projektfortschritt - Details") self.setMinimumSize(800, 800) self.process_steps = process_steps # Main layout for the dialog main_layout = QVBoxLayout(self) # Scrollable area for process steps scroll_area = QScrollArea(self) scroll_area.setWidgetResizable(True) # Container widget and layout for process steps process_widget = QFrame() process_layout = QVBoxLayout(process_widget) for step in self.process_steps: # Each process step will have its own section step_layout = QVBoxLayout() step_label = QLabel(f"{step['name']}") step_layout.addWidget(step_label) # Status and required files progress = self.get_step_progress(step) progress_bar = QProgressBar() progress_bar.setValue(int(progress)) progress_bar.setTextVisible(True) step_layout.addWidget(progress_bar) # Description of the step description_label = QLabel(f"Beschreibung: {step['description']}") step_layout.addWidget(description_label) # Show CSV creation and geocoding status for first step if "csv_creation_status" in step and "geocoding_status" in step: # CSV Creation Status csv_status_text = self.get_status_text(step["csv_creation_status"]) csv_status_color = self.get_status_color(step["csv_creation_status"]) csv_label = QLabel(f"CSV-Erstellung: {csv_status_text}") csv_label.setStyleSheet(f"color: {csv_status_color}; font-weight: bold;") step_layout.addWidget(csv_label) # Geocoding Status geocoding_status_text = self.get_status_text(step["geocoding_status"]) geocoding_status_color = self.get_status_color(step["geocoding_status"]) geocoding_label = QLabel(f"Geocoding: {geocoding_status_text}") geocoding_label.setStyleSheet(f"color: {geocoding_status_color}; font-weight: bold;") step_layout.addWidget(geocoding_label) # Show missing files if any if len(step.get("missing_files", [])) > 0: for missing_file in step["missing_files"]: missing_label = QLabel(f"Fehlende Datei: {missing_file}") missing_label.setStyleSheet("color: red;") step_layout.addWidget(missing_label) else: completed_label = QLabel("Alle Dateien vorhanden") completed_label.setStyleSheet("color: green;") step_layout.addWidget(completed_label) # Optionally, add file sizes and modification times if available if "file_sizes" in step: for _i, size in enumerate(step["file_sizes"]): size_label = QLabel(f"Dateigröße: {size}") step_layout.addWidget(size_label) if "file_modification_times" in step: for _i, mod_time in enumerate(step["file_modification_times"]): mod_time_label = QLabel(f"Zuletzt geändert: {mod_time}") step_layout.addWidget(mod_time_label) # Add the step layout to the process layout step_frame = QFrame() step_frame.setLayout(step_layout) process_layout.addWidget(step_frame) # Set the widget for the scroll area scroll_area.setWidget(process_widget) main_layout.addWidget(scroll_area) # Close button close_button = QPushButton("Schließen") close_button.clicked.connect(self.accept) main_layout.addWidget(close_button)
[docs] def get_step_progress(self, step): """ Calculate step completion percentage based on required files. :param step: Process step information. :type step: dict :return: Completion percentage (0-100). :rtype: float """ total_files = len(step["required_files"]) missing_files = len(step.get("missing_files", [])) return ((total_files - missing_files) / total_files) * 100
[docs] def get_status_text(self, status): """ Get human-readable status text. :param status: Status code. :type status: str :return: Human-readable status text. :rtype: str """ status_mapping = { "completed": "Abgeschlossen", "pending": "Ausstehend", "not_applicable": "Nicht verfügbar", "not_checked": "Nicht geprüft", } return status_mapping.get(status, "Unbekannt")
[docs] def get_status_color(self, status): """ Get color for status display. :param status: Status code. :type status: str :return: CSS color value. :rtype: str """ color_mapping = {"completed": "green", "pending": "orange", "not_applicable": "gray", "not_checked": "gray"} return color_mapping.get(status, "black")
[docs] class BuildingCSVDialog(QDialog): """ Dialog for tabular input and editing of Quartier IST.csv building data. """
[docs] def __init__(self, headers, data=None, parent=None): """ Initialize dialog for building CSV creation/editing. :param headers: Column headers for table. :type headers: list :param data: Initial table data. :type data: list of lists :param parent: Parent widget. :type parent: QWidget """ super().__init__(parent) self.setWindowTitle("Gebäudedaten bearbeiten") self.resize(1200, 600) self.headers = headers self.data = data if data is not None else [] self.initUI()
[docs] def initUI(self): """ Initialize user interface components. """ layout = QVBoxLayout(self) label = QLabel("Geben Sie die Gebäudedaten für das Quartier ein:") layout.addWidget(label) self.table = QTableWidget(self) self.table.setColumnCount(len(self.headers)) self.table.setHorizontalHeaderLabels(self.headers) self.table.setRowCount(len(self.data)) # Enhanced table formatting self.table.setAlternatingRowColors(True) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.table.setSortingEnabled(True) self.table.horizontalHeader().setStretchLastSection(True) for row_idx, row_data in enumerate(self.data): for col_idx, value in enumerate(row_data): item = QTableWidgetItem(str(value)) self.table.setItem(row_idx, col_idx, item) layout.addWidget(self.table) # Buttons for row operations button_layout = QHBoxLayout() add_row_btn = QPushButton("Zeile hinzufügen") del_row_btn = QPushButton("Zeile löschen") add_row_btn.clicked.connect(self.add_row) del_row_btn.clicked.connect(self.del_row) button_layout.addWidget(add_row_btn) button_layout.addWidget(del_row_btn) layout.addLayout(button_layout) # Dialog buttons buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) buttonBox.accepted.connect(self.accept) buttonBox.rejected.connect(self.reject) layout.addWidget(buttonBox)
[docs] def add_row(self): """ Add new row to table. """ self.table.insertRow(self.table.rowCount())
[docs] def del_row(self): """ Delete selected row from table. """ currentRow = self.table.currentRow() if currentRow > -1: self.table.removeRow(currentRow)
[docs] def get_table_data(self): """ Get table data as list of lists. :return: Table data. :rtype: list of lists """ rows = self.table.rowCount() cols = self.table.columnCount() data = [] for r in range(rows): row_data = [] for c in range(cols): item = self.table.item(r, c) row_data.append(item.text() if item else "") data.append(row_data) return data