Source code for districtheatingsim.gui.ProjectTab.project_tab

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

Project management tab with MVP architecture for CSV file editing and project tracking.

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

import os
import sys
import csv
import json

from PyQt6.QtWidgets import (QMainWindow, QFileDialog, QTableWidgetItem, QWidget, QVBoxLayout, QHBoxLayout,
                             QMenuBar, QProgressBar, QLabel, QTableWidget, QFrame,
                             QTreeView, QSplitter, QMessageBox, QDialog, QMenu, QPushButton, QInputDialog, QSizePolicy)
from PyQt6.QtGui import QAction, QFileSystemModel
from PyQt6.QtCore import Qt, QTimer

from geopy.geocoders import Nominatim
from pyproj import Transformer

from districtheatingsim.gui.LeafletTab.net_generation_threads import GeocodingThread, GeoJSONToCSVThread
from districtheatingsim.gui.ProjectTab.project_tab_dialogs import RowInputDialog, OSMImportDialog, ProcessDetailsDialog

[docs] class ProjectModel: """ Model for managing project data including CSV and GeoJSON file operations. """
[docs] def __init__(self): self.base_path = None self.current_file_path = '' self.layers = {}
[docs] def set_base_path(self, base_path): """ Set project base path. :param base_path: Project base path. :type base_path: str """ self.base_path = base_path
[docs] def get_base_path(self): """ Get project base path. :return: Current base path. :rtype: str """ return self.base_path
[docs] def load_csv(self, file_path): """ Load CSV file data. :param file_path: Path to CSV file. :type file_path: str :return: Headers and data lists. :rtype: tuple """ with open(file_path, 'r', encoding='utf-8') as file: reader = csv.reader(file, delimiter=';') headers = next(reader) data = [row for row in reader] return headers, data
[docs] def save_csv(self, file_path, headers, data): """ Save data to CSV file. :param file_path: Output file path. :type file_path: str :param headers: Column headers. :type headers: list :param data: Table data. :type data: list of lists """ with open(file_path, 'w', newline='', encoding='utf-8-sig') as file: writer = csv.writer(file, delimiter=';') writer.writerow(headers) writer.writerows(data)
[docs] def create_csv(self, file_path, headers, default_data): """ Create new CSV file with default data. :param file_path: Output file path. :type file_path: str :param headers: Column headers. :type headers: list :param default_data: Default row data. :type default_data: list """ with open(file_path, 'w', newline='', encoding='utf-8-sig') as file: writer = csv.writer(file, delimiter=';') writer.writerow(headers) writer.writerow(default_data)
[docs] def create_csv_from_geojson(self, geojson_file_path, output_file_path, default_values): """ Create CSV from GeoJSON data with default values. :param geojson_file_path: Input GeoJSON file path. :type geojson_file_path: str :param output_file_path: Output CSV file path. :type output_file_path: str :param default_values: Default values for building parameters. :type default_values: dict :return: Output file path. :rtype: str """ try: with open(geojson_file_path, 'r') as geojson_file: data = json.load(geojson_file) # Initialize geocoder and transformer once geolocator = Nominatim(user_agent="DistrictHeatingSim") transformer = Transformer.from_crs("epsg:25833", "epsg:4326", always_xy=True) with open(output_file_path, 'w', encoding='utf-8-sig', newline='') as csvfile: fieldnames = ["Land", "Bundesland", "Stadt", "Adresse", "Wärmebedarf", "Gebäudetyp", "Subtyp", "WW_Anteil", "Typ_Heizflächen", "VLT_max", "Steigung_Heizkurve", "RLT_max", "Normaußentemperatur", "UTM_X", "UTM_Y"] writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=";") writer.writeheader() for i, feature in enumerate(data['features']): centroid = self.calculate_centroid(feature['geometry']['coordinates']) # Reverse geocode each building individually land = default_values.get("Land", "Deutschland") bundesland = default_values.get("Bundesland", "") stadt = default_values.get("Stadt", "") adresse = default_values.get("Adresse", "") if centroid[0] is not None and centroid[1] is not None: try: # Transform UTM to WGS84 lon, lat = transformer.transform(centroid[0], centroid[1]) # Reverse geocode with timeout location = geolocator.reverse(f"{lat}, {lon}", language="de", timeout=10) if location and location.raw.get('address'): address_data = location.raw['address'] # Extract address components land = address_data.get('country', land) bundesland = address_data.get('state', bundesland) stadt = address_data.get('city') or address_data.get('town') or address_data.get('village') or address_data.get('municipality') or stadt # Build street address street_parts = [] if 'road' in address_data: street_parts.append(address_data['road']) if 'house_number' in address_data: street_parts.append(address_data['house_number']) if street_parts: adresse = " ".join(street_parts) print(f"Gebäude {i+1}: {adresse}, {stadt}") except Exception as e: print(f"Reverse Geocoding für Gebäude {i+1} fehlgeschlagen: {e}") writer.writerow({ "Land": land, "Bundesland": bundesland, "Stadt": stadt, "Adresse": adresse, "Wärmebedarf": default_values["Wärmebedarf"], "Gebäudetyp": default_values["Gebäudetyp"], "Subtyp": default_values["Subtyp"], "WW_Anteil": default_values["WW_Anteil"], "Typ_Heizflächen": default_values["Typ_Heizflächen"], "VLT_max": default_values["VLT_max"], "Steigung_Heizkurve": default_values["Steigung_Heizkurve"], "RLT_max": default_values["RLT_max"], "Normaußentemperatur": default_values["Normaußentemperatur"], "UTM_X": centroid[0], "UTM_Y": centroid[1] }) return output_file_path except Exception as e: raise Exception(f"Fehler beim Erstellen der CSV-Datei: {str(e)}")
[docs] def calculate_centroid(self, coordinates): """ Calculate centroid of coordinate array. :param coordinates: Coordinate array. :type coordinates: list :return: Centroid coordinates (x, y). :rtype: tuple """ x_sum = 0 y_sum = 0 total_points = 0 if isinstance(coordinates[0], float): x_sum += coordinates[0] y_sum += coordinates[1] total_points += 1 else: for item in coordinates: x, y = self.calculate_centroid(item) if x is not None and y is not None: x_sum += x y_sum += y total_points += 1 if total_points > 0: centroid_x = x_sum / total_points centroid_y = y_sum / total_points return centroid_x, centroid_y else: return None, None
[docs] class ProjectPresenter: """ Presenter managing interaction between ProjectModel and ProjectTabView. """
[docs] def __init__(self, model, view, folder_manager, data_manager, config_manager): """ Initialize project presenter. :param model: Data model. :type model: ProjectModel :param view: View component. :type view: ProjectTabView :param folder_manager: Folder manager. :type folder_manager: object :param data_manager: Data manager. :type data_manager: object :param config_manager: Configuration manager. :type config_manager: object """ self.model = model self.view = view self.folder_manager = folder_manager self.data_manager = data_manager self.config_manager = config_manager # Define process steps for project tracking self.process_steps = [ { "name": "Schritt 1: Gebäudedaten Quartier definieren", "description": "Erstellen Sie die Gebäude-CSV hier im Tab 'Projektdefinition'. Die CSV kann manuell erstellt, aus GeoJSON importiert oder durch Geocoding mit Koordinaten angereichert werden.", "required_files": [ "..\\Definition Quartier IST\\Quartier IST.csv" ], "csv_creation_status": "not_checked", # Will be updated dynamically "geocoding_status": "not_checked" # Will be updated dynamically }, { "name": "Schritt 2: Gebäude-Lastgang generieren", "description": "Generieren Sie den Gebäude-Lastgang im Tab 'Wärmebedarf Gebäude' ", "required_files": [ "Lastgang\\Gebäude Lastgang.json" ] }, { "name": "Schritt 3: Straßendaten herunterladen", "description": "Führen Sie eine OSM-Straßenabfrage im Tab 'Wärmenetz generieren' durch.", "required_files": [ "..\\Eingangsdaten allgemein\\Straßen.geojson" ] }, { "name": "Schritt 3: Wärmenetz Daten erstellen", "description": "Generieren Sie das Wärmenetz im Tab 'Wärmenetz generieren'.", "required_files": [ "Wärmenetz\\Wärmenetz.geojson" ] }, { "name": "Schritt 4: Thermohydraulische Berechnung", "description": "Führen Sie die Thermohydraulische Berechnung mit den generierten Netzdaten durch.", "required_files": [ "Wärmenetz\\Ergebnisse Netzinitialisierung.p", "Wärmenetz\\Ergebnisse Netzinitialisierung.csv", "Wärmenetz\\Konfiguration Netzinitialisierung.json", "Lastgang\\Lastgang.csv" ], "check_dimensioned_network": True # Special check for dimensioned flag in Wärmenetz.geojson }, { "name": "Schritt 5: Erzeugermix auslegen und berechnen", "description": "Berechnen sie den Erzeugermix und speichern sie die Ergebnisse.", "required_files": [ "Ergebnisse\\calculated_heat_generation.csv", "Ergebnisse\\Ergebnisse.json" ] } ] # Connect signals and initialize (only after view is set) self.folder_manager.project_folder_changed.connect(self.on_variant_folder_changed) # Progress update timer self.timer = QTimer() self.timer.timeout.connect(self.update_progress_tracker) self.timer.start(50000)
[docs] def connect_view_signals(self): """ Connect view signals after view is created. """ if self.view: self.view.treeView.doubleClicked.connect(self.on_tree_view_double_clicked) # Initial update after view is available if self.folder_manager.variant_folder: self.on_variant_folder_changed(self.folder_manager.variant_folder) self.update_progress_tracker()
[docs] def on_variant_folder_changed(self, path): """ Handle project folder change. :param path: New project folder path. :type path: str """ if path: self.model.set_base_path(path) if self.view: # Only update if view exists self.view.update_tree_view(os.path.dirname(path)) if self.view: # Only update progress if view exists self.update_progress_tracker()
[docs] def on_tree_view_double_clicked(self, index): """ Handle double-click on file tree view. :param index: Tree view index. :type index: QModelIndex """ """Handle tree view double-click events.""" file_path = self.view.get_selected_file_path(index) if os.path.isdir(file_path): if "Variante" in os.path.basename(file_path): self.folder_manager.set_variant_folder(file_path) else: self.folder_manager.set_project_folder(file_path) elif file_path.endswith('.csv'): self.load_csv(file_path)
[docs] def import_csv(self): """ Open file dialog to import existing CSV file. """ """Open CSV file dialog and load selected file.""" standard_path = os.path.join(self.folder_manager.get_variant_folder(), self.config_manager.get_relative_path("current_building_data_path")) fname, _ = QFileDialog.getOpenFileName(self.view, 'CSV öffnen', standard_path, 'CSV Files (*.csv);;All Files (*)') if fname and fname.strip(): # Check for valid file path self.load_csv(fname)
[docs] def load_csv(self, file_path): """ Load CSV file into table view. :param file_path: Path to CSV file. :type file_path: str """ headers, data = self.model.load_csv(file_path) self.model.current_file_path = file_path self.view.csvTable.setRowCount(0) self.view.csvTable.setColumnCount(len(headers)) self.view.csvTable.setHorizontalHeaderLabels(headers) for row_data in data: row = self.view.csvTable.rowCount() self.view.csvTable.insertRow(row) for column, cell_value in enumerate(row_data): item = QTableWidgetItem(cell_value) self.view.csvTable.setItem(row, column, item)
[docs] def save_csv(self, show_dialog=True): """ Save current table data to CSV file. :param show_dialog: Show confirmation dialog if True. :type show_dialog: bool """ headers = [self.view.csvTable.horizontalHeaderItem(i).text() for i in range(self.view.csvTable.columnCount())] data = [[self.view.csvTable.item(row, column).text() if self.view.csvTable.item(row, column) else '' for column in range(self.view.csvTable.columnCount())] for row in range(self.view.csvTable.rowCount())] file_path = self.model.current_file_path if not file_path: # Use default path if no file is open file_path = os.path.join(self.folder_manager.get_variant_folder(), self.config_manager.get_relative_path("current_building_data_path")) self.model.current_file_path = file_path try: self.model.save_csv(file_path, headers, data) if show_dialog: self.view.show_message("Erfolg", f"CSV-Datei wurde in {file_path} gespeichert.") except Exception as e: if show_dialog: self.view.show_error_message("Fehler", str(e))
[docs] def add_row(self): """ Add new empty row to table. """ self.view.csvTable.insertRow(self.view.csvTable.rowCount())
[docs] def del_row(self): """ Delete selected row from table. """ currentRow = self.view.csvTable.currentRow() if currentRow > -1: self.view.csvTable.removeRow(currentRow) else: self.view.show_error_message("Warnung", "Bitte wählen Sie eine Zeile zum Löschen aus.")
[docs] def create_csv(self, fname=None, show_dialog=True): """ Create new CSV file with default building data headers. :param fname: Optional file path. :type fname: str :param show_dialog: Show file dialog if True. :type show_dialog: bool """ headers = ['Land', 'Bundesland', 'Stadt', 'Adresse', 'Wärmebedarf', 'Gebäudetyp', "Subtyp", 'WW_Anteil', 'Typ_Heizflächen', 'VLT_max', 'Steigung_Heizkurve', 'RLT_max', "Normaußentemperatur"] default_data = ['']*len(headers) if not fname: fname = os.path.join(self.folder_manager.get_variant_folder(), self.config_manager.get_relative_path("current_building_data_path")) if show_dialog: fname_dialog, _ = QFileDialog.getSaveFileName(self.view, 'Gebäude-CSV erstellen', fname, 'CSV Files (*.csv);;All Files (*)') if fname_dialog and fname_dialog.strip(): # Check for valid file path fname = fname_dialog else: return # User cancelled dialog if fname and fname.strip(): # Additional check for valid filename self.model.create_csv(fname, headers, default_data) self.load_csv(fname) if show_dialog: self.view.show_message("Erfolg", f"CSV-Datei wurde in {fname} erstellt.")
[docs] def create_csv_from_geojson(self): """ Create CSV from GeoJSON with user-defined building parameters. """ standard_path = os.path.join(self.folder_manager.get_variant_folder(), self.config_manager.get_relative_path("OSM_buldings_path")) geojson_file_path, _ = QFileDialog.getOpenFileName(self.view, "geoJSON auswählen", standard_path, "All Files (*)") if geojson_file_path and geojson_file_path.strip(): # Check for valid file path # Extract sample coordinates from first building for reverse geocoding sample_coords = None try: with open(geojson_file_path, 'r') as f: data = json.load(f) if data.get('features') and len(data['features']) > 0: first_feature = data['features'][0] centroid = self.model.calculate_centroid(first_feature['geometry']['coordinates']) if centroid[0] is not None and centroid[1] is not None: sample_coords = centroid except Exception as e: print(f"Konnte keine Beispielkoordinaten extrahieren: {e}") dialog = OSMImportDialog(self.view, sample_utm_coords=sample_coords) if dialog.exec() == QDialog.DialogCode.Accepted: default_values = dialog.get_input_data() standard_output_path = os.path.join(self.folder_manager.get_variant_folder(), self.config_manager.get_relative_path("OSM_building_data_path")) output_file_path = standard_output_path # Start conversion in thread if hasattr(self, 'geojson_conversion_thread') and self.geojson_conversion_thread.isRunning(): self.geojson_conversion_thread.stop() self.geojson_conversion_thread.wait() self.geojson_conversion_thread = GeoJSONToCSVThread( geojson_file_path, output_file_path, default_values, self.model ) self.geojson_conversion_thread.progress_update.connect(self.on_geojson_conversion_progress) self.geojson_conversion_thread.calculation_done.connect(self.on_geojson_conversion_done) self.geojson_conversion_thread.calculation_error.connect(self.on_geojson_conversion_error) self.geojson_conversion_thread.start() # Show progress bar self.view.progressBar.setRange(0, 0) # Indeterminate until we know total self.view.statusLabel.setText("Konvertiere GeoJSON zu CSV mit Reverse Geocoding...")
[docs] def on_geojson_conversion_progress(self, current, total, message): """ Handle progress updates from GeoJSON conversion thread. :param current: Current building number. :type current: int :param total: Total number of buildings. :type total: int :param message: Progress message. :type message: str """ if total > 0: self.view.progressBar.setRange(0, total) self.view.progressBar.setValue(current) self.view.statusLabel.setText(message)
[docs] def on_geojson_conversion_done(self, output_file_path): """ Handle completion of GeoJSON conversion. :param output_file_path: Path to created CSV file. :type output_file_path: str """ self.view.progressBar.setRange(0, 1) self.view.progressBar.setValue(1) self.view.statusLabel.setText("Konvertierung abgeschlossen") self.load_csv(output_file_path) self.view.show_message("Erfolg", f"CSV-Datei wurde erfolgreich erstellt:\n{output_file_path}")
[docs] def on_geojson_conversion_error(self, error_message): """ Handle error during GeoJSON conversion. :param error_message: Error message. :type error_message: str """ self.view.progressBar.setRange(0, 1) self.view.progressBar.setValue(0) self.view.statusLabel.setText("Fehler bei der Konvertierung") self.view.show_error_message("Fehler", error_message)
[docs] def geocode_current_csv(self): """ Geocode the currently loaded CSV file. """ if hasattr(self.model, 'current_file_path') and self.model.current_file_path: # Save current table data first self.save_csv(show_dialog=False) # Then geocode the saved file self.geocode_addresses(self.model.current_file_path) else: self.view.show_error_message("Fehler", "Keine CSV-Datei geladen. Bitte laden Sie zuerst eine CSV-Datei oder erstellen Sie eine neue.")
[docs] def open_geocode_addresses_dialog(self): """ Open file dialog for geocoding CSV selection. """ standard_path = os.path.join(self.folder_manager.get_variant_folder(), self.config_manager.get_relative_path("current_building_data_path")) fname, _ = QFileDialog.getOpenFileName(self.view, 'CSV-Koordinaten laden', standard_path, 'CSV Files (*.csv);;All Files (*)') if fname and fname.strip(): # Check for valid file path self.geocode_addresses(fname)
[docs] def geocode_addresses(self, inputfilename): """ Start geocoding thread for address processing. :param inputfilename: Path to CSV file for geocoding. :type inputfilename: str """ if hasattr(self, 'geocodingThread') and self.geocodingThread.isRunning(): self.geocodingThread.terminate() self.geocodingThread.wait() self.geocodingThread = GeocodingThread(inputfilename) self.geocodingThread.calculation_done.connect(self.on_geocode_done) self.geocodingThread.calculation_error.connect(self.on_geocode_error) self.geocodingThread.start() self.view.progressBar.setRange(0, 0)
[docs] def on_geocode_done(self, fname): """ Handle geocoding completion. :param fname: Output filename. :type fname: str """ self.view.progressBar.setRange(0, 1) # Automatically reload the updated CSV file to show the new coordinates if fname and os.path.exists(fname): self.load_csv(fname) # Update the progress tracker to refresh CSV status (should now show "mit Koordinaten") self.update_progress_tracker() self.view.show_message("Erfolg", "Geocoding abgeschlossen. CSV-Datei wurde automatisch aktualisiert.")
[docs] def on_geocode_error(self, error_message): """ Handle geocoding errors. :param error_message: Error message to display. :type error_message: str """ self.view.show_error_message("Fehler beim Geocoding", error_message) self.view.progressBar.setRange(0, 1)
[docs] def update_progress(self, progress, csv_status=None): """ Update progress bar value and CSV status label. :param progress: Progress percentage. :type progress: float :param csv_status: Status text for Quartier IST.csv. :type csv_status: str """ self.projectProgressBar.setValue(int(progress)) if csv_status is not None: self.csv_status_label.setText(f"Quartier IST.csv Status: {csv_status}")
[docs] def check_csv_status(self, csv_file_path): """ Check detailed CSV status: missing, available without coordinates, or with coordinates. :param csv_file_path: Path to CSV file to check. :type csv_file_path: str :return: Status: 'fehlt', 'ist vorhanden', or 'mit Koordinaten'. :rtype: str """ if not os.path.exists(csv_file_path): return 'fehlt' try: with open(csv_file_path, 'r', encoding='utf-8', errors='ignore') as file: # Use semicolon delimiter to match the CSV format reader = csv.DictReader(file, delimiter=';') headers = reader.fieldnames if not headers: return 'ist vorhanden' # Check if UTM coordinate columns exist coord_columns = ['UTM_X', 'UTM_Y'] has_coord_headers = all(col in headers for col in coord_columns) if not has_coord_headers: return 'ist vorhanden' # Check if coordinate columns have data for row in reader: if 'UTM_X' in row and 'UTM_Y' in row and row['UTM_X'] and row['UTM_Y']: if row['UTM_X'].strip() and row['UTM_Y'].strip(): try: x_val = float(row['UTM_X']) y_val = float(row['UTM_Y']) return 'mit Koordinaten' # Found at least one valid coordinate pair except ValueError: continue # Only check first few rows for performance break return 'ist vorhanden' # Has headers but no valid coordinate data except Exception as e: # If we can't read the CSV, assume it exists but is problematic return 'ist vorhanden'
[docs] def check_network_dimensioned(self, network_file_path): """ Check if network GeoJSON has state set to "dimensioned". :param network_file_path: Path to Wärmenetz.geojson file. :type network_file_path: str :return: True if network is dimensioned, False otherwise. :rtype: bool """ if not os.path.exists(network_file_path): return False try: from districtheatingsim.net_generation.network_geojson_schema import NetworkGeoJSONSchema geojson = NetworkGeoJSONSchema.import_from_file(network_file_path) # Check metadata for state == "dimensioned" metadata = geojson.get('metadata', {}) state = metadata.get('state', '') return state == 'dimensioned' except Exception as e: print(f"Fehler beim Prüfen des Dimensionierungsstatus: {e}") return False
[docs] def update_progress_tracker(self): """ Update project progress and CSV status label based on file existence and content. """ if not self.view: # Skip if view not available yet return base_path = self.model.get_base_path() # CSV Status: check first process step (Quartier IST.csv) with detailed analysis csv_status = "unbekannt" if base_path: # Check first process step for Quartier IST.csv first_step = self.process_steps[0] csv_file_path = os.path.join(base_path, first_step['required_files'][0]) csv_status = self.check_csv_status(csv_file_path) # Update CSV creation and geocoding status for first step if os.path.exists(csv_file_path): first_step['csv_creation_status'] = 'completed' # Check if CSV has coordinates (UTM_X and UTM_Y columns) if csv_status == 'mit Koordinaten': first_step['geocoding_status'] = 'completed' elif csv_status == 'ist vorhanden': first_step['geocoding_status'] = 'pending' else: first_step['geocoding_status'] = 'not_applicable' else: first_step['csv_creation_status'] = 'pending' first_step['geocoding_status'] = 'not_applicable' # Update all process steps for step in self.process_steps: full_paths = [os.path.join(base_path, path) for path in step['required_files']] generated_files = [file for file in full_paths if os.path.exists(file)] # Special check for dimensioned network flag in Wärmenetz.geojson if step.get('check_dimensioned_network', False): network_file = os.path.join(base_path, "Wärmenetz\\Wärmenetz.geojson") network_dimensioned = self.check_network_dimensioned(network_file) if not network_dimensioned: # Add virtual missing file indicator step['missing_files'] = [path for path in full_paths if not os.path.exists(path)] step['missing_files'].append("Wärmenetz\\Wärmenetz.geojson (nicht dimensioniert)") step['completed'] = False else: step['missing_files'] = [path for path in full_paths if not os.path.exists(path)] step['completed'] = len(step['missing_files']) == 0 else: step['completed'] = len(generated_files) == len(full_paths) step['missing_files'] = [path for path in full_paths if not os.path.exists(path)] else: for step in self.process_steps: step['completed'] = False step['missing_files'] = step['required_files'] total_steps = len(self.process_steps) completed_steps = sum(1 for step in self.process_steps if step['completed']) overall_progress = (completed_steps / total_steps) * 100 self.view.update_progress(overall_progress, csv_status=csv_status) self.view.set_process_steps(self.process_steps)
[docs] class ProjectTabView(QWidget): """ View component for project tab UI with file browser and CSV editor. """
[docs] def __init__(self, presenter=None, parent=None): """ Initialize project tab view. :param presenter: Presenter instance for signal connections. :type presenter: ProjectPresenter :param parent: Parent widget. :type parent: QWidget """ super().__init__(parent) self.presenter = presenter self.initUI()
[docs] def initUI(self): """ Initialize user interface components. """ mainLayout = QVBoxLayout() splitter = QSplitter() # Left area - file browser and progress self.leftLayout = QVBoxLayout() self.model = QFileSystemModel() self.model.setRootPath("") self.treeView = QTreeView() self.treeView.setModel(self.model) self.treeView.setMinimumWidth(500) self.treeView.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # Projektfortschritt oben self.progressLayout = QHBoxLayout() self.progressLabel = QLabel("Projektfortschritt:") self.progressLayout.addWidget(self.progressLabel) self.projectProgressBar = QProgressBar(self) self.progressLayout.addWidget(self.projectProgressBar) # Details Button self.detailsButton = QPushButton("Details Projektfortschritt anzeigen", self) self.detailsButton.setToolTip("Details zu den einzelnen Schritten anzeigen") self.detailsButton.clicked.connect(self.showDetailsDialog) self.progressLayout.addWidget(self.detailsButton) self.leftLayout.addLayout(self.progressLayout) # CSV-Status oben self.csv_status_label = QLabel("❓ Quartier IST.csv: Status unbekannt") self.csv_status_label.setStyleSheet("font-weight: bold; color: #757575; background-color: #f5f5f5; padding: 8px; border-radius: 4px; border-left: 4px solid #757575; margin-bottom: 10px;") self.leftLayout.addWidget(self.csv_status_label) # Button-Bar für alle Aktionen button_layout = QHBoxLayout() self.csv_import_button = QPushButton("CSV importieren") self.csv_import_button.setToolTip("Bestehende Gebäude-CSV importieren und ins Projekt kopieren") button_layout.addWidget(self.csv_import_button) self.csv_create_button = QPushButton("CSV erstellen") self.csv_create_button.setToolTip("Neue Gebäude-CSV erstellen") button_layout.addWidget(self.csv_create_button) self.csv_save_button = QPushButton("CSV speichern") self.csv_save_button.setToolTip("CSV aus Tabelle speichern") button_layout.addWidget(self.csv_save_button) self.csv_from_osm_button = QPushButton("CSV aus OSM-GeoJSON") self.csv_from_osm_button.setToolTip("Gebäude-CSV aus OSM-GeoJSON generieren") button_layout.addWidget(self.csv_from_osm_button) self.geocode_button = QPushButton("Geokoordinaten berechnen") self.geocode_button.setToolTip("Aktuelle CSV-Datei geocodieren und Koordinaten hinzufügen") button_layout.addWidget(self.geocode_button) button_frame = QFrame() button_frame.setLayout(button_layout) button_frame.setFrameShape(QFrame.Shape.StyledPanel) self.leftLayout.addWidget(button_frame) leftWidget = QWidget() leftWidget.setLayout(self.leftLayout) self.leftLayout.addWidget(self.treeView) splitter.addWidget(leftWidget) # Right area - CSV editor self.rightLayout = QVBoxLayout() # Menüleiste entfällt, alle Aktionen sind Buttons self.csvTable = QTableWidget() self.csvTable.setEditTriggers(QTableWidget.EditTrigger.DoubleClicked | QTableWidget.EditTrigger.EditKeyPressed) self.csvTable.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.csvTable.customContextMenuRequested.connect(self.show_context_menu) # Enhanced table formatting self.csvTable.setAlternatingRowColors(True) self.csvTable.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.csvTable.setSortingEnabled(True) self.csvTable.horizontalHeader().setStretchLastSection(True) # Set row height for better visibility self.csvTable.verticalHeader().setDefaultSectionSize(35) self.csvTable.verticalHeader().setMinimumSectionSize(30) self.rightLayout.addWidget(self.csvTable) # Status label for operations self.statusLabel = QLabel("") self.statusLabel.setStyleSheet("padding: 5px; color: #666666;") self.rightLayout.addWidget(self.statusLabel) self.progressBar = QProgressBar(self) self.rightLayout.addWidget(self.progressBar) rightWidget = QWidget() rightWidget.setLayout(self.rightLayout) splitter.addWidget(rightWidget) splitter.setStretchFactor(1, 2) mainLayout.addWidget(splitter) self.setLayout(mainLayout) # Button-Signale verbinden if self.presenter: self.csv_create_button.clicked.connect(self.presenter.create_csv) self.csv_import_button.clicked.connect(self.presenter.import_csv) self.csv_save_button.clicked.connect(lambda: self.presenter.save_csv(show_dialog=True)) self.csv_from_osm_button.clicked.connect(self.presenter.create_csv_from_geojson) self.geocode_button.clicked.connect(self.presenter.geocode_current_csv)
[docs] def update_tree_view(self, path): """ Update file tree view root path. :param path: New root path. :type path: str """ self.treeView.setRootIndex(self.treeView.model().index(path)) for column in range(self.model.columnCount()): self.treeView.resizeColumnToContents(column)
[docs] def show_context_menu(self, position): """ Show table context menu. :param position: Menu position. :type position: QPoint """ contextMenu = QMenu(self) addRowAction = QAction("Gebäude hinzufügen", self) deleteRowAction = QAction("Gebäude löschen", self) duplicateRowAction = QAction("Gebäude duplizieren", self) contextMenu.addAction(addRowAction) contextMenu.addAction(deleteRowAction) contextMenu.addAction(duplicateRowAction) addRowAction.triggered.connect(self.add_row) deleteRowAction.triggered.connect(self.delete_row) duplicateRowAction.triggered.connect(self.duplicate_row) contextMenu.exec(self.csvTable.viewport().mapToGlobal(position))
[docs] def add_row(self): """ Add new table row with input dialog. """ headers = [self.csvTable.horizontalHeaderItem(i).text() for i in range(self.csvTable.columnCount())] dialog = RowInputDialog(headers, self) if dialog.exec() == QDialog.DialogCode.Accepted: row_data = dialog.get_input_data() row = self.csvTable.rowCount() self.csvTable.insertRow(row) for i, header in enumerate(headers): self.csvTable.setItem(row, i, QTableWidgetItem(row_data[header]))
[docs] def delete_row(self): """ Delete selected table row. """ currentRow = self.csvTable.currentRow() if currentRow > -1: self.csvTable.removeRow(currentRow) else: self.show_error_message("Warnung", "Bitte wählen Sie ein Gebäude zum Löschen aus.")
[docs] def duplicate_row(self): """ Duplicate selected table row. """ currentRow = self.csvTable.currentRow() if currentRow > -1: row = self.csvTable.rowCount() self.csvTable.insertRow(row) for column in range(self.csvTable.columnCount()): item = self.csvTable.item(currentRow, column) newItem = QTableWidgetItem(item.text() if item else '') self.csvTable.setItem(row, column, newItem) else: self.show_error_message("Warnung", "Bitte wählen Sie ein Gebäude zum Duplizieren aus.")
[docs] def get_selected_file_path(self, index): """ Get selected file path from tree view. :param index: Tree view index. :type index: QModelIndex :return: Selected file path. :rtype: str """ return self.model.filePath(index)
[docs] def show_error_message(self, title, message): """ Display error message dialog. :param title: Dialog title. :type title: str :param message: Error message. :type message: str """ QMessageBox.critical(self, title, message)
[docs] def update_progress(self, progress, csv_status=None): """ Update progress bar value and CSV status label with color-coded status. :param progress: Progress percentage. :type progress: float :param csv_status: Status text for Quartier IST.csv. :type csv_status: str """ self.projectProgressBar.setValue(int(progress)) if csv_status is not None: # Define status with icons and colors status_config = { 'fehlt': { 'text': '❌ Quartier IST.csv: Datei fehlt', 'style': 'font-weight: bold; color: #d32f2f; background-color: #ffebee; padding: 8px; border-radius: 4px; border-left: 4px solid #d32f2f;' }, 'ist vorhanden': { 'text': '⚠️ Quartier IST.csv: Ist vorhanden (ohne Koordinaten)', 'style': 'font-weight: bold; color: #f57c00; background-color: #fff3e0; padding: 8px; border-radius: 4px; border-left: 4px solid #f57c00;' }, 'mit Koordinaten': { 'text': '✅ Quartier IST.csv: Mit Koordinaten (vollständig)', 'style': 'font-weight: bold; color: #388e3c; background-color: #e8f5e8; padding: 8px; border-radius: 4px; border-left: 4px solid #388e3c;' }, 'unbekannt': { 'text': '❓ Quartier IST.csv: Status unbekannt', 'style': 'font-weight: bold; color: #757575; background-color: #f5f5f5; padding: 8px; border-radius: 4px; border-left: 4px solid #757575;' } } config = status_config.get(csv_status, status_config['unbekannt']) self.csv_status_label.setText(config['text']) self.csv_status_label.setStyleSheet(config['style'])
[docs] def showDetailsDialog(self): """ Show process step details dialog. """ dialog = ProcessDetailsDialog(self.process_steps, self) dialog.exec()
[docs] def set_process_steps(self, process_steps): """ Set process steps data for details dialog. :param process_steps: List of process step dictionaries. :type process_steps: list """ self.process_steps = process_steps
[docs] def show_message(self, title, message): """ Display information message dialog. :param title: Dialog title. :type title: str :param message: Information message. :type message: str """ QMessageBox.information(self, title, message)
[docs] class ProjectTab(QMainWindow): """ Main project tab window integrating MVP components. .. note:: Central interface for project management with file operations and progress tracking functionality. """
[docs] def __init__(self, folder_manager, data_manager, config_manager, parent=None): """ Initialize project tab with MVP architecture. :param folder_manager: Folder manager. :type folder_manager: object :param data_manager: Data manager. :type data_manager: object :param config_manager: Configuration manager. :type config_manager: object :param parent: Parent widget. :type parent: QWidget """ super().__init__() self.setWindowTitle("Project Tab Example") self.setGeometry(100, 100, 800, 600) self.model = ProjectModel() self.presenter = ProjectPresenter(self.model, None, folder_manager, data_manager, config_manager) self.view = ProjectTabView(presenter=self.presenter) self.presenter.view = self.view self.presenter.connect_view_signals() self.setCentralWidget(self.view)