diff --git a/.github/workflows/test_plugin.yml b/.github/workflows/test_plugin.yml index 1048cdf..eb861ad 100644 --- a/.github/workflows/test_plugin.yml +++ b/.github/workflows/test_plugin.yml @@ -33,7 +33,7 @@ jobs: strategy: matrix: - docker_tags: [release-3_28, latest] + docker_tags: [release-3_28, release-3_36] steps: @@ -45,12 +45,7 @@ jobs: - name: Docker pull and create qgis-testing-environment run: | docker pull "$DOCKER_IMAGE":${{ matrix.docker_tags }} - docker run -d -e XDG_RUNTIME_DIR=/tmp/runtime-root --name qgis-testing-environment -v ${{ github.workspace }}:/tests_directory -e DISPLAY=:99 "$DOCKER_IMAGE":${{ matrix.docker_tags }} - - - name: List mounted directory contents - run: | - docker exec qgis-testing-environment ls -la /tests_directory - docker exec qgis-testing-environment ls -la /tests_directory/$PLUGIN_NAME + docker run -d -e XDG_RUNTIME_DIR=/tmp/runtime-root --name qgis-testing-environment -v ${{ github.workspace }}:/tests_directory -e DISPLAY=:99 "$DOCKER_IMAGE":${{ matrix.docker_tags }} tail -f /dev/null - name: Docker set up QGIS run: | @@ -67,10 +62,16 @@ jobs: run: | docker exec qgis-testing-environment sh -c "touch /tests_directory/$PLUGIN_NAME/REMEDY_GIS_RiskTool/__init__.py" docker exec qgis-testing-environment ls -la /tests_directory/$PLUGIN_NAME/REMEDY_GIS_RiskTool + + - name: Install and start Xvfb inside Docker container + run: | + docker exec qgis-testing-environment apt update + docker exec qgis-testing-environment apt install -y xvfb + docker exec qgis-testing-environment Xvfb :99 -screen 0 1024x768x24 & - name: Docker run plugin tests run: | - docker exec qgis-testing-environment sh -c "export PYTHONPATH=/root/.local/share/QGIS/QGIS3/profiles/default/python/plugins:$PYTHONPATH && qgis_testrunner.sh $TESTS_RUN_FUNCTION" + docker exec qgis-testing-environment sh -c "export DISPLAY=:99; qgis_testrunner.sh $TESTS_RUN_FUNCTION" Check-code-quality: runs-on: ubuntu-latest diff --git a/geovita_processing_plugin/REMEDY_GIS_RiskTool b/geovita_processing_plugin/REMEDY_GIS_RiskTool index 10d02a8..de3bb63 160000 --- a/geovita_processing_plugin/REMEDY_GIS_RiskTool +++ b/geovita_processing_plugin/REMEDY_GIS_RiskTool @@ -1 +1 @@ -Subproject commit 10d02a89f9fba7c7a8927b941a1dd2b3d41a9cae +Subproject commit de3bb6330c407450876d38639f99bbeb3864cfe1 diff --git a/geovita_processing_plugin/__init__.py b/geovita_processing_plugin/__init__.py index 9f66177..019b598 100644 --- a/geovita_processing_plugin/__init__.py +++ b/geovita_processing_plugin/__init__.py @@ -26,7 +26,7 @@ __date__ = '2024-01-17' __copyright__ = '(C) 2024 by DPE' -__version__ = "3.1.1" +__version__ = "3.2.0" import sys from pathlib import Path diff --git a/geovita_processing_plugin/algorithms/BegrensSkadeExcavation.py b/geovita_processing_plugin/algorithms/BegrensSkadeExcavation.py index 98cde27..4218ccc 100644 --- a/geovita_processing_plugin/algorithms/BegrensSkadeExcavation.py +++ b/geovita_processing_plugin/algorithms/BegrensSkadeExcavation.py @@ -32,6 +32,7 @@ import traceback from pathlib import Path +from datetime import datetime from qgis.core import ( Qgis, @@ -49,11 +50,12 @@ QgsProcessingParameterRasterLayer, QgsProcessingParameterString, QgsProject, + QgsVectorLayer, + QgsProcessingOutputFile, ) from qgis.PyQt.QtCore import QCoreApplication from ..REMEDY_GIS_RiskTool.BegrensSkade import mainBegrensSkade_Excavation -from ..utilities.AddLayersTask import AddLayersTask from ..utilities.gui import GuiUtils from ..utilities.logger import CustomLogger from ..utilities.methodslib import ( @@ -117,7 +119,6 @@ def __init__(self): self.feature_name = None # Default value self.layers_info = {} self.styles_dir_path = Path() - self.add_layers_task = AddLayersTask() self.logger.info("__INIT__ - Finished initialize BegrensSkadeExcavation ") def tr(self, string): @@ -205,15 +206,10 @@ def __getstate__(self): "RASTER_ROCK_SURFACE", "Input raster of depth to bedrock", ] - POREPRESSURE_ENUM = ["POREPRESSURE_ENUM", "Pore pressure reduction curves"] - enum_porepressure = [ - "Lav poretrykksreduksjon", - "Middels poretrykksreduksjon", - "Høy poretrykksreduksjon", - ] - POREPRESSURE_REDUCTION = [ - "POREPRESSURE_REDUCTION", - "Porepressure reduction [kPa]", + + POREWP_REDUCTION_M = [ + "POREWP_REDUCTION_M", + "Porewater pressure reduction [m]", ] DRY_CRUST_THICKNESS = [ "DRY_CRUST_THICKNESS", @@ -287,7 +283,7 @@ def initAlgorithm(self, config): self.EXCAVATION_DEPTH[0], self.tr(f"{self.EXCAVATION_DEPTH[1]}"), QgsProcessingParameterNumber.Double, - defaultValue=0, + defaultValue=10, minValue=0, ) param.setFlags(QgsProcessingParameterDefinition.FlagAdvanced) @@ -322,24 +318,16 @@ def initAlgorithm(self, config): | QgsProcessingParameterDefinition.FlagOptional ) self.addParameter(param) - - param = QgsProcessingParameterEnum( - self.POREPRESSURE_ENUM[0], - self.tr(f"{self.POREPRESSURE_ENUM[1]}"), - self.enum_porepressure, - defaultValue=1, - allowMultiple=False, - ) - param.setFlags(QgsProcessingParameterDefinition.FlagAdvanced) - self.addParameter(param) + param = QgsProcessingParameterNumber( - self.POREPRESSURE_REDUCTION[0], - self.tr(f"{self.POREPRESSURE_REDUCTION[1]}"), - defaultValue=50, + self.POREWP_REDUCTION_M[0], + self.tr(f"{self.POREWP_REDUCTION_M[1]}"), + defaultValue=10, minValue=0, ) param.setFlags(QgsProcessingParameterDefinition.FlagAdvanced) self.addParameter(param) + param = QgsProcessingParameterNumber( self.DRY_CRUST_THICKNESS[0], self.tr(f"{self.DRY_CRUST_THICKNESS[1]}"), @@ -462,8 +450,9 @@ def initAlgorithm(self, config): self.OUTPUT_FEATURE_NAME, self.tr( "Naming Conventions for Analysis and Features (Output feature name appended to file-names)" - ), - ) + ) + ), + createOutput=True ) self.addParameter( QgsProcessingParameterCrs( @@ -478,6 +467,24 @@ def initAlgorithm(self, config): self.tr("Output Folder"), ) ) + self.addOutput( + QgsProcessingOutputFile( + self.OUTPUT_BUILDING, + self.tr("Output Buildings Shapefile"), + ) + ) + self.addOutput( + QgsProcessingOutputFile( + self.OUTPUT_WALL, + self.tr("Output Walls Shapefile"), + ) + ) + self.addOutput( + QgsProcessingOutputFile( + self.OUTPUT_CORNER, + self.tr("Output Corners Shapefile"), + ) + ) self.logger.info("initAlgorithm - Done setting up the inputs.") @@ -661,10 +668,6 @@ def processAlgorithm(self, parameters, context, feedback): ) return {} - porepressure_index = self.parameterAsEnum( - parameters, self.POREPRESSURE_ENUM[0], context - ) - pw_reduction_curve = self.enum_porepressure[porepressure_index] dry_crust_thk = self.parameterAsDouble( parameters, self.DRY_CRUST_THICKNESS[0], context ) @@ -675,8 +678,8 @@ def processAlgorithm(self, parameters, context, feedback): parameters, self.SOIL_DENSITY[0], context ) ocr_value = self.parameterAsDouble(parameters, self.OCR[0], context) - porewp_red = self.parameterAsInt( - parameters, self.POREPRESSURE_REDUCTION[0], context + porewp_red_m = self.parameterAsInt( + parameters, self.POREWP_REDUCTION_M[0], context ) janbu_ref_stress = self.parameterAsInt( parameters, self.JANBU_REF_STRESS[0], context @@ -693,12 +696,11 @@ def processAlgorithm(self, parameters, context, feedback): else: path_source_raster_rock_surface = None - pw_reduction_curve = None dry_crust_thk = None dep_groundwater = None density_sat = None ocr_value = None - porewp_red = None + porewp_red_m = None janbu_ref_stress = None janbu_const = None janbu_m = None @@ -769,12 +771,11 @@ def processAlgorithm(self, parameters, context, feedback): feedback.pushInfo( f"PROCESS - Param: dtb_raster = {path_source_raster_rock_surface}" ) - feedback.pushInfo(f"PROCESS - Param: pw_reduction_curve = {pw_reduction_curve}") feedback.pushInfo(f"PROCESS - Param: dry_crust_thk = {dry_crust_thk}") feedback.pushInfo(f"PROCESS - Param: dep_groundwater = {dep_groundwater}") feedback.pushInfo(f"PROCESS - Param: density_sat = {density_sat}") feedback.pushInfo(f"PROCESS - Param: OCR = {ocr_value}") - feedback.pushInfo(f"PROCESS - Param: porewp_red = {porewp_red}") + feedback.pushInfo(f"PROCESS - Param: porewp_red_m = {porewp_red_m}") feedback.pushInfo(f"PROCESS - Param: janbu_ref_stress = {janbu_ref_stress}") feedback.pushInfo(f"PROCESS - Param: janbu_const = {janbu_const}") feedback.pushInfo(f"PROCESS - Param: janbu_m = {janbu_m}") @@ -797,12 +798,11 @@ def processAlgorithm(self, parameters, context, feedback): short_term_curve=short_term_curve, bLongterm=bLongterm, dtb_raster=str(path_source_raster_rock_surface), - pw_reduction_curve=pw_reduction_curve, dry_crust_thk=dry_crust_thk, dep_groundwater=dep_groundwater, density_sat=density_sat, OCR=ocr_value, - porewp_red=porewp_red, + porewp_red_m=porewp_red_m, janbu_ref_stress=janbu_ref_stress, janbu_const=janbu_const, janbu_m=janbu_m, @@ -821,7 +821,7 @@ def processAlgorithm(self, parameters, context, feedback): return {} #################### HANDLE THE RESULT ############################### - feedback.setProgress(80) + feedback.setProgress(90) self.logger.info(f"PROCESS - OUTPUT BUILDINGS: {output_shapefiles[0]}") self.logger.info(f"PROCESS - OUTPUT WALL: {output_shapefiles[1]}") self.logger.info(f"PROCESS - OUTPUT CORNER: {output_shapefiles[2]}") @@ -840,80 +840,89 @@ def processAlgorithm(self, parameters, context, feedback): "shape_path": output_shapefiles[1], "style_name": "WALL-ANGLE.qml", }, - "BUILDING-TOTAL-SETTLMENT": { - "shape_path": output_shapefiles[0], - "style_name": "BUILDING-TOTAL-SETTLMENT_sv_tot.qml", - }, "BUILDING-TOTAL-ANGLE": { "shape_path": output_shapefiles[0], "style_name": "BUILDING-TOTAL-ANGLE_max_angle.qml", }, + "BUILDING-TOTAL-SETTLMENT": { + "shape_path": output_shapefiles[0], + "style_name": "BUILDING-TOTAL-SETTLMENT_sv_tot.qml", + } } if bVulnerability: self.layers_info.update( { - "BUILDING-RISK-SETTLMENT": { - "shape_path": output_shapefiles[0], - "style_name": "BUILDING-TOTAL-RISK-SELLMENT_risk_tots.qml", - }, "BUILDING-RISK-ANGLE": { "shape_path": output_shapefiles[0], "style_name": "BUILDING-TOTAL-RISK-ANGLE_risk_angle.qml", }, + "BUILDING-RISK-SETTLMENT": { + "shape_path": output_shapefiles[0], + "style_name": "BUILDING-TOTAL-RISK-SELLMENT_risk_tots.qml", + } } ) - feedback.setProgress(90) + feedback.setProgress(100) feedback.pushInfo("PROCESS - Finished processing!") + # Return the results of the algorithm. - return { - self.OUTPUT_BUILDING: output_shapefiles[0], - self.OUTPUT_WALL: output_shapefiles[1], - self.OUTPUT_CORNER: output_shapefiles[2], - } - + return {self.OUTPUT_BUILDING: output_shapefiles[0], + self.OUTPUT_WALL: output_shapefiles[1], + self.OUTPUT_CORNER: output_shapefiles[2], + + } + def postProcessAlgorithm(self, context, feedback): """ - Handles the post-processing steps of the algorithm, specifically adding output layers to the QGIS project. - - This method creates and executes a process to add layers to the QGIS interface, applying predefined styles - and organizing them within a specified group. It leverages the `AddLayersTask` class to manage layer - addition in a way that ensures thread safety and proper GUI updates. - - Parameters: - - context (QgsProcessingContext): The context of the processing, providing access to the QGIS project and other relevant settings. - - feedback (QgsProcessingFeedback): The object used to report progress and log messages back to the user. - - Returns: - - dict: An empty dictionary. This method does not produce output parameters but instead focuses on the side effect of adding layers to the project. - - Note: - This method sets up a task for layer addition, defining success and failure callbacks to provide user feedback. - It manually starts the process and handles its completion. + This method is called after processAlgorithm finishes. + Here, we manually load the output shapefiles, apply QML styles, + and place them under a custom group in the layer tree. """ - ######### EXPERIMENTAL ADD LAYERS TO GUI ######### - # Create the task - self.add_layers_task.setParameters( - self.layers_info, - self.feature_name, - self.styles_dir_path, - self.logger, - ) - - # Define a slot to handle the task completion - def onTaskCompleted(success): - if success: - feedback.pushInfo("POSTPROCESS - Layers added successfully.") - feedback.setProgress(100) + project = context.project() + root = project.layerTreeRoot() + + # Create (or find) a group at the top level named by 'self.feature_name'. + group_name = self.feature_name + group = root.findGroup(group_name) + if not group: + group = root.insertGroup(0, group_name) + + # 2) Loop through the entries stored in 'self.layers_info' defined during processAlgorithm + for layer_label, layer_info in self.layers_info.items(): + shape_path = layer_info["shape_path"] # e.g. "C:/somefolder/buildings.shp" + style_name = layer_info["style_name"] # e.g. "BUILDING-TOTAL-SETTLMENT_sv_tot.qml" + + # Generate a unique layer name with timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M") + final_layer_name = f"{layer_label}_{timestamp}" + + # Create the QgsVectorLayer from the file path + layer = QgsVectorLayer(shape_path, final_layer_name, "ogr") + if not layer.isValid(): + feedback.reportError(f"Could not load layer from file: {shape_path}") + continue + + # Load the QML style if it exists + style_path = self.styles_dir_path / style_name # e.g. /path/to/styles/BUILDING-TOTAL-SETTLMENT_sv_tot.qml + if style_path.is_file(): + layer.loadNamedStyle(str(style_path)) + layer.triggerRepaint() else: - feedback.reportError("POSTPROCESS - Failed to add layers.") - feedback.setProgress(100) + feedback.reportError(f"Style file not found: {style_path}") + + # Add the layer to the project (layer registry) *without* adding to the root TOC + QgsProject.instance().addMapLayer(layer, False) + + # Place the layer under the group at the bottom + group.insertLayer(-1, layer) - # Connect the task's completed signal to the slot - self.add_layers_task.taskCompleted.connect(onTaskCompleted) + # Ensure the new layer node is visible + node = group.findLayer(layer.id()) + if node: + node.setItemVisibilityChecked(True) - # Start the task - success = self.add_layers_task.run() - self.add_layers_task.finished(success) + feedback.pushInfo(f"Loaded and styled layer '{final_layer_name}' in group '{group_name}'.") - return {} + feedback.pushInfo("postProcessAlgorithm complete.") + return {} \ No newline at end of file diff --git a/geovita_processing_plugin/algorithms/BegrensSkadeImpactMap.py b/geovita_processing_plugin/algorithms/BegrensSkadeImpactMap.py index f6c3dc7..91ca043 100644 --- a/geovita_processing_plugin/algorithms/BegrensSkadeImpactMap.py +++ b/geovita_processing_plugin/algorithms/BegrensSkadeImpactMap.py @@ -45,13 +45,15 @@ QgsProcessingParameterDefinition, QgsProcessingParameterRasterLayer, QgsMessageLog, + QgsProcessingOutputFile, + QgsRasterLayer ) import traceback +from datetime import datetime from pathlib import Path from .base_algorithm import GvBaseProcessingAlgorithms -from ..utilities.AddLayersTask import AddLayersTask from ..utilities.gui import GuiUtils from ..utilities.logger import CustomLogger from ..utilities.methodslib import ( @@ -116,7 +118,6 @@ def __init__(self): self.feature_name = None # Default value self.layers_info = {} self.styles_dir_path = Path() - self.add_layers_task = AddLayersTask() self.logger.info("__INIT__ - Finished initialize BegrensSkadeImpactMap ") def name(self): @@ -200,13 +201,8 @@ def icon(self): "CLIPPING_RANGE", "Clip distance in case of high resolution (buffer distance in [meters])", ] - POREPRESSURE_ENUM = ["POREPRESSURE_ENUM", "Pore pressure reduction curves"] - enum_porepressure = [ - "Lav poretrykksreduksjon", - "Middels poretrykksreduksjon", - "Høy poretrykksreduksjon", - ] - POREPRESSURE_REDUCTION = ["POREPRESSURE_REDUCTION", "Porepressure reduction [kPa]"] + + POREWP_REDUCTION_M = ["POREWP_REDUCTION_M", "Porewater pressure reduction [m]"] DRY_CRUST_THICKNESS = [ "DRY_CRUST_THICKNESS", "Thickness of overburden not affected by porewater drawdown [m]", @@ -242,14 +238,6 @@ def initAlgorithm(self, config): ) self.addParameter(param) - # DEFINE OUTPUTS - # We add a outputs to store our processed features - # self.addParameter(QgsProcessingParameterRasterDestination( - # self.OUTPUT_RASTER, - # self.tr("Output Raster") - # ) - # ) - self.addParameter( QgsProcessingParameterString( self.OUTPUT_FEATURE_NAME, @@ -313,19 +301,11 @@ def initAlgorithm(self, config): ) param.setFlags(QgsProcessingParameterDefinition.FlagAdvanced) self.addParameter(param) - param = QgsProcessingParameterEnum( - self.POREPRESSURE_ENUM[0], - self.tr(f"{self.POREPRESSURE_ENUM[1]}"), - self.enum_porepressure, - defaultValue=1, - allowMultiple=False, - ) - param.setFlags(QgsProcessingParameterDefinition.FlagAdvanced) - self.addParameter(param) + param = QgsProcessingParameterNumber( - self.POREPRESSURE_REDUCTION[0], - self.tr(f"{self.POREPRESSURE_REDUCTION[1]}"), - defaultValue=50, + self.POREWP_REDUCTION_M[0], + self.tr(f"{self.POREWP_REDUCTION_M[1]}"), + defaultValue=10, minValue=0, ) param.setFlags(QgsProcessingParameterDefinition.FlagAdvanced) @@ -396,6 +376,14 @@ def initAlgorithm(self, config): ) param.setFlags(QgsProcessingParameterDefinition.FlagAdvanced) self.addParameter(param) + + # We add the output definition + self.addOutput( + QgsProcessingOutputFile( + self.OUTPUT_RASTER, + self.tr("Output Raster impact map"), + ) + ) def processAlgorithm(self, parameters, context, feedback): """ @@ -421,13 +409,11 @@ def processAlgorithm(self, parameters, context, feedback): clipping_range = self.parameterAsInt( parameters, self.CLIPPING_RANGE[0], context ) - porepressure_index = self.parameterAsEnum( - parameters, self.POREPRESSURE_ENUM[0], context - ) + output_resolution = self.parameterAsDouble( parameters, self.OUTPUT_RESOLUTION[0], context ) - pw_reduction_curve = self.enum_porepressure[porepressure_index] + dry_crust_thk = self.parameterAsDouble( parameters, self.DRY_CRUST_THICKNESS[0], context ) @@ -436,8 +422,8 @@ def processAlgorithm(self, parameters, context, feedback): ) density_sat = self.parameterAsDouble(parameters, self.SOIL_DENSITY[0], context) ocr_value = self.parameterAsDouble(parameters, self.OCR[0], context) - porewp_red = self.parameterAsInt( - parameters, self.POREPRESSURE_REDUCTION[0], context + porewp_red_m = self.parameterAsInt( + parameters, self.POREWP_REDUCTION_M[0], context ) janbu_ref_stress = self.parameterAsInt( parameters, self.JANBU_REF_STRESS[0], context @@ -579,12 +565,11 @@ def processAlgorithm(self, parameters, context, feedback): feedback.pushInfo(f"PROCESS - PARAM CALCULATION_RANGE: {clipping_range}") feedback.pushInfo(f"PROCESS - PARAM output_proj: {output_srid}") feedback.pushInfo(f"PROCESS - PARAM dtb_raster: {str(path_processed_raster)}") - feedback.pushInfo(f"PROCESS - PARAM pw_reduction_curve: {pw_reduction_curve}") feedback.pushInfo(f"PROCESS - PARAM dry_crust_thk: {dry_crust_thk}") feedback.pushInfo(f"PROCESS - PARAM dep_groundwater: {dep_groundwater}") feedback.pushInfo(f"PROCESS - PARAM density_sat: {density_sat}") feedback.pushInfo(f"PROCESS - PARAM OCR: {ocr_value}") - feedback.pushInfo(f"PROCESS - PARAM porewp_red: {porewp_red}") + feedback.pushInfo(f"PROCESS - PARAM porewp_red_m: {porewp_red_m}") feedback.pushInfo(f"PROCESS - PARAM janbu_ref_stress: {janbu_ref_stress}") feedback.pushInfo(f"PROCESS - PARAM janbu_const: {janbu_const}") feedback.pushInfo(f"PROCESS - PARAM janbu_m: {janbu_m}") @@ -604,12 +589,11 @@ def processAlgorithm(self, parameters, context, feedback): CALCULATION_RANGE=clipping_range, # '380' hardcoded constant used in the underlying submodule's method. output_proj=output_srid, dtb_raster=str(path_processed_raster), - pw_reduction_curve=pw_reduction_curve, dry_crust_thk=dry_crust_thk, dep_groundwater=dep_groundwater, density_sat=density_sat, OCR=ocr_value, - porewp_red=porewp_red, + porewp_red_m=porewp_red_m, janbu_ref_stress=janbu_ref_stress, janbu_const=janbu_const, janbu_m=janbu_m, @@ -642,49 +626,62 @@ def processAlgorithm(self, parameters, context, feedback): } } - feedback.setProgress(90) + feedback.setProgress(100) feedback.pushInfo("PROCESS - Finished processing!") # Return the results of the algorithm. - return {self.OUTPUT_FOLDER: output_raster_path} + return {self.OUTPUT_RASTER: output_raster_path} def postProcessAlgorithm(self, context, feedback): """ - Handles the post-processing steps of the algorithm, specifically adding output layers to the QGIS project. - - This method creates and executes a process to add layers to the QGIS interface, applying predefined styles - and organizing them within a specified group. It leverages the `AddLayersTask` class to manage layer - addition in a way that ensures thread safety and proper GUI updates. - - Parameters: - - context (QgsProcessingContext): The context of the processing, providing access to the QGIS project and other relevant settings. - - feedback (QgsProcessingFeedback): The object used to report progress and log messages back to the user. + After processAlgorithm finishes, load the produced raster (output_raster_path), + apply a QML style, and place it into a custom group in the TOC. + """ + project = context.project() + root = project.layerTreeRoot() + + # 1) Define or find the group at the top level + group_name = self.feature_name + group = root.findGroup(group_name) + if not group: + group = root.insertGroup(0, group_name) + + # The self.layers_info just have one key: "IMPACT-MAP", + # but let's loop in case there are added more raster layers in future + for layer_label, layer_info in self.layers_info.items(): + raster_path = layer_info["shape_path"] + style_name = layer_info["style_name"] + style_path = self.styles_dir_path / style_name + + # Build a unique name with timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M") + final_layer_name = f"{layer_label}_{timestamp}" + + # Create a QgsRasterLayer + # Use "gdal" or an appropriate provider for your raster format + raster_layer = QgsRasterLayer(raster_path, final_layer_name, "gdal") + if not raster_layer.isValid(): + feedback.reportError(f"Invalid raster layer from file: {raster_path}") + continue + + # Attempt to load a QML style (if it references valid raster symbology) + if style_path.is_file(): + raster_layer.loadNamedStyle(str(style_path)) + raster_layer.triggerRepaint() + else: + feedback.reportError(f"Style file not found: {style_path}") - Returns: - - dict: An empty dictionary. This method does not produce output parameters but instead focuses on the side effect of adding layers to the project. + # Add the layer to the project, but do *not* place it in the root + QgsProject.instance().addMapLayer(raster_layer, False) - Note: - This method sets up a task for layer addition, defining success and failure callbacks to provide user feedback. - It manually starts the process and handles its completion. - """ - ######### EXPERIMENTAL ADD LAYERS TO GUI ######### - # Create the task - self.add_layers_task.setParameters( - self.layers_info, self.feature_name, self.styles_dir_path, self.logger - ) + # Insert it in our custom group + group.insertLayer(0, raster_layer) - # Define a slot to handle the task completion - def onTaskCompleted(success): - if success: - feedback.pushInfo("POSTPROCESS - Layers added successfully.") - feedback.setProgress(100) - else: - feedback.reportError("POSTPROCESS - Failed to add layers.") - feedback.setProgress(100) + # Make sure it's visible + node = group.findLayer(raster_layer.id()) + if node: + node.setItemVisibilityChecked(True) - # Connect the task's completed signal to the slot - self.add_layers_task.taskCompleted.connect(onTaskCompleted) + feedback.pushInfo(f"Loaded and styled raster layer '{final_layer_name}' in group '{group_name}'.") - # Start the task - success = self.add_layers_task.run() - self.add_layers_task.finished(success) + feedback.pushInfo("postProcessAlgorithm complete.") return {} diff --git a/geovita_processing_plugin/algorithms/BegrensSkadeTunnel.py b/geovita_processing_plugin/algorithms/BegrensSkadeTunnel.py index 4d61872..29d2a51 100644 --- a/geovita_processing_plugin/algorithms/BegrensSkadeTunnel.py +++ b/geovita_processing_plugin/algorithms/BegrensSkadeTunnel.py @@ -46,16 +46,18 @@ QgsProcessingParameterEnum, QgsProcessingParameterField, QgsProcessingParameterRasterLayer, + QgsProcessingOutputFile, + QgsVectorLayer ) from .base_algorithm import GvBaseProcessingAlgorithms import traceback from pathlib import Path +from datetime import datetime from ..REMEDY_GIS_RiskTool.BegrensSkade import mainBegrensSkade_Tunnel -from ..utilities.AddLayersTask import AddLayersTask from ..utilities.gui import GuiUtils from ..utilities.logger import CustomLogger from ..utilities.methodslib import ( @@ -117,7 +119,6 @@ def __init__(self): self.feature_name = None # Default value self.layers_info = {} self.styles_dir_path = Path() - self.add_layers_task = AddLayersTask() self.logger.info("__INIT__ - Finished initialize BegrensSkadeTunnel ") def name(self): @@ -204,9 +205,9 @@ def icon(self): "TUNNEL_LEAKAGE", "Leakage of water into the tunnel [L/min each 100m of tunnelsection]", ] - POREPRESSURE_REDUCTION = [ - "POREPRESSURE_REDUCTION", - 'Porepressure reduction with tunnel (only used if the curve is "Manual" [kPa]', + POREWP_REDUCTION = [ + "POREWP_REDUCTION", + 'Porewater pressure reduction at tunnel (only used if the curve is "Manual") [m]', ] DRY_CRUST_THICKNESS = [ "DRY_CRUST_THICKNESS", @@ -352,8 +353,8 @@ def initAlgorithm(self, config): self.addParameter(param) param = QgsProcessingParameterNumber( - self.POREPRESSURE_REDUCTION[0], - self.tr(f"{self.POREPRESSURE_REDUCTION[1]}"), + self.POREWP_REDUCTION[0], + self.tr(f"{self.POREWP_REDUCTION[1]}"), defaultValue=0, optional=True, minValue=0, @@ -498,6 +499,24 @@ def initAlgorithm(self, config): self.tr("Output Folder"), ) ) + self.addOutput( + QgsProcessingOutputFile( + self.OUTPUT_BUILDING, + self.tr("Output Buildings Shapefile"), + ) + ) + self.addOutput( + QgsProcessingOutputFile( + self.OUTPUT_WALL, + self.tr("Output Walls Shapefile"), + ) + ) + self.addOutput( + QgsProcessingOutputFile( + self.OUTPUT_CORNER, + self.tr("Output Corners Shapefile"), + ) + ) self.logger.info("initAlgorithm - Done setting up the inputs.") @@ -691,8 +710,8 @@ def processAlgorithm(self, parameters, context, feedback): tunnel_leakage = self.parameterAsDouble( parameters, self.TUNNEL_LEAKAGE[0], context ) - porewp_red_at_site = self.parameterAsInt( - parameters, self.POREPRESSURE_REDUCTION[0], context + porewp_red_at_site_m = self.parameterAsInt( + parameters, self.POREWP_REDUCTION[0], context ) dry_crust_thk = self.parameterAsDouble( parameters, self.DRY_CRUST_THICKNESS[0], context @@ -719,7 +738,7 @@ def processAlgorithm(self, parameters, context, feedback): else: porewp_calc_type = None - porewp_red_at_site = None + porewp_red_at_site_m = 0 tunnel_leakage = None path_source_raster_rock_surface = None dry_crust_thk = None @@ -797,7 +816,7 @@ def processAlgorithm(self, parameters, context, feedback): feedback.pushInfo(f"PROCESS - Param: bLongterm = {bLongterm}") feedback.pushInfo(f"PROCESS - Param: tunnel_leakage = {tunnel_leakage}") feedback.pushInfo(f"PROCESS - Param: porewp_calc_type = {porewp_calc_type}") - feedback.pushInfo(f"PROCESS - Param: porewp_red_at_site = {porewp_red_at_site}") + feedback.pushInfo(f"PROCESS - Param: porewp_red_at_site_m = {porewp_red_at_site_m}") feedback.pushInfo( f"PROCESS - Param: dtb_raster = {path_source_raster_rock_surface}" ) @@ -830,7 +849,7 @@ def processAlgorithm(self, parameters, context, feedback): bLongterm=bLongterm, tunnel_leakage=tunnel_leakage, porewp_calc_type=porewp_calc_type, - porewp_red_at_site=porewp_red_at_site, + porewp_red_at_site_m=porewp_red_at_site_m, dtb_raster=str(path_source_raster_rock_surface), dry_crust_thk=dry_crust_thk, dep_groundwater=dep_groundwater, @@ -872,30 +891,30 @@ def processAlgorithm(self, parameters, context, feedback): "shape_path": output_shapefiles[1], "style_name": "WALL-ANGLE.qml", }, - "TUNNEL_BUILDING-TOTAL-SETTLMENT": { - "shape_path": output_shapefiles[0], - "style_name": "BUILDING-TOTAL-SETTLMENT_sv_tot.qml", - }, "TUNNEL_BUILDING-TOTAL-ANGLE": { "shape_path": output_shapefiles[0], "style_name": "BUILDING-TOTAL-ANGLE_max_angle.qml", }, + "TUNNEL_BUILDING-TOTAL-SETTLMENT": { + "shape_path": output_shapefiles[0], + "style_name": "BUILDING-TOTAL-SETTLMENT_sv_tot.qml", + } } if bVulnerability: self.layers_info.update( { - "TUNNEL_BUILDING-RISK-SETTLMENT": { - "shape_path": output_shapefiles[0], - "style_name": "BUILDING-TOTAL-RISK-SELLMENT_risk_tots.qml", - }, "TUNNEL_BUILDING-RISK-ANGLE": { "shape_path": output_shapefiles[0], "style_name": "BUILDING-TOTAL-RISK-ANGLE_risk_angle.qml", }, + "TUNNEL_BUILDING-RISK-SETTLMENT": { + "shape_path": output_shapefiles[0], + "style_name": "BUILDING-TOTAL-RISK-SELLMENT_risk_tots.qml", + } } ) - feedback.setProgress(90) + feedback.setProgress(100) feedback.pushInfo("PROCESS - Finished processing!") # Return the results of the algorithm. return { @@ -903,45 +922,58 @@ def processAlgorithm(self, parameters, context, feedback): self.OUTPUT_WALL: output_shapefiles[1], self.OUTPUT_CORNER: output_shapefiles[2], } - + + def postProcessAlgorithm(self, context, feedback): """ - Handles the post-processing steps of the algorithm, specifically adding output layers to the QGIS project. - - This method creates and executes a process to add layers to the QGIS interface, applying predefined styles - and organizing them within a specified group. It leverages the `AddLayersTask` class to manage layer - addition in a way that ensures thread safety and proper GUI updates. + This method is called after processAlgorithm finishes. + Here, we manually load the output shapefiles, apply QML styles, + and place them under a custom group in the layer tree. + """ + project = context.project() + root = project.layerTreeRoot() + + # Create (or find) a group at the top level named by 'self.feature_name'. + group_name = self.feature_name + group = root.findGroup(group_name) + if not group: + group = root.insertGroup(0, group_name) + + # 2) Loop through the entries stored in 'self.layers_info' defined during processAlgorithm + for layer_label, layer_info in self.layers_info.items(): + shape_path = layer_info["shape_path"] # e.g. "C:/somefolder/buildings.shp" + style_name = layer_info["style_name"] # e.g. "BUILDING-TOTAL-SETTLMENT_sv_tot.qml" + + # Generate a unique layer name with timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M") + final_layer_name = f"{layer_label}_{timestamp}" + + # Create the QgsVectorLayer from the file path + layer = QgsVectorLayer(shape_path, final_layer_name, "ogr") + if not layer.isValid(): + feedback.reportError(f"Could not load layer from file: {shape_path}") + continue + + # Load the QML style if it exists + style_path = self.styles_dir_path / style_name # e.g. /path/to/styles/BUILDING-TOTAL-SETTLMENT_sv_tot.qml + if style_path.is_file(): + layer.loadNamedStyle(str(style_path)) + layer.triggerRepaint() + else: + feedback.reportError(f"Style file not found: {style_path}") - Parameters: - - context (QgsProcessingContext): The context of the processing, providing access to the QGIS project and other relevant settings. - - feedback (QgsProcessingFeedback): The object used to report progress and log messages back to the user. + # Add the layer to the project (layer registry) *without* adding to the root TOC + QgsProject.instance().addMapLayer(layer, False) - Returns: - - dict: An empty dictionary. This method does not produce output parameters but instead focuses on the side effect of adding layers to the project. + # Place the layer under the group at the bottom + group.insertLayer(-1, layer) - Note: - This method sets up a task for layer addition, defining success and failure callbacks to provide user feedback. - It manually starts the process and handles its completion. - """ - ######### EXPERIMENTAL ADD LAYERS TO GUI ######### - # Create the task - self.add_layers_task.setParameters( - self.layers_info, self.feature_name, self.styles_dir_path, self.logger - ) - - # Define a slot to handle the task completion - def onTaskCompleted(success): - if success: - feedback.pushInfo("POSTPROCESS - Layers added successfully.") - feedback.setProgress(100) - else: - feedback.reportError("POSTPROCESS - Failed to add layers.") - feedback.setProgress(100) + # Ensure the new layer node is visible + node = group.findLayer(layer.id()) + if node: + node.setItemVisibilityChecked(True) - # Connect the task's completed signal to the slot - self.add_layers_task.taskCompleted.connect(onTaskCompleted) + feedback.pushInfo(f"Loaded and styled layer '{final_layer_name}' in group '{group_name}'.") - # Start the task - success = self.add_layers_task.run() - self.add_layers_task.finished(success) - return {} + feedback.pushInfo("postProcessAlgorithm complete.") + return {} \ No newline at end of file diff --git a/geovita_processing_plugin/metadata.txt b/geovita_processing_plugin/metadata.txt index 1f83dbd..61a16f1 100644 --- a/geovita_processing_plugin/metadata.txt +++ b/geovita_processing_plugin/metadata.txt @@ -7,7 +7,7 @@ name=Geovita GIS Processing provider qgisMinimumVersion=3.28 qgisMaximumVersion=3.99 description=This plugin adds different Geovita custom processing algorithms to QGIS -version=3.1.1 +version=3.2.0 author=DPE email=dpe@geovita.no @@ -28,7 +28,11 @@ repository=https://github.com/danpejobo/geovita_processing_plugin hasProcessingProvider=yes # Uncomment the following line and add your changelog: -changelog=v.3.1.1 (04.03.2024) +changelog=v.3.2.0 (13.03.2025) + - Uppdate submodule to newest upstream. + - Rewrite logic for adding and styling layers. Now uses postProcessing. Results should be added to TOC, and visible on the canvas + + v.3.1.1 (04.03.2024) - Add version logging and feedback. - Fix for two layers having the same name in the excavation algorithm (thus only one showing in TOC) - Fix "label" attributes for the styles to the excavation and tunnel algorithms diff --git a/geovita_processing_plugin/test/test_alg_excavation.py b/geovita_processing_plugin/test/test_alg_excavation.py index 4065e55..d6a1678 100644 --- a/geovita_processing_plugin/test/test_alg_excavation.py +++ b/geovita_processing_plugin/test/test_alg_excavation.py @@ -121,8 +121,7 @@ def setUp(self): "SETTLEMENT_ENUM": 1, # index "LONG_TERM_SETTLEMENT": True, "RASTER_ROCK_SURFACE": self.raster_rock_surface_layer, - "POREPRESSURE_ENUM": 1, # index - "POREPRESSURE_REDUCTION": 50, + "POREWP_REDUCTION_M": 10, "DRY_CRUST_THICKNESS": 5.0, "DEPTH_GROUNDWATER": 3, "SOIL_DENSITY": 18.5, @@ -241,7 +240,7 @@ def test_output_verification_with_all_params(self): # Check if the counted features match the expected value expected_feature_corner_count = ( - 11 # Hypothetical expected number of features with sv_tot > 0.1 + 565 # Hypothetical expected number of features with sv_tot > 0.1 ) self.assertEqual( feature_count_sv_tot_greater_than_01, @@ -254,18 +253,18 @@ def test_output_verification_with_all_params(self): results["OUTPUT_WALL"], "Output Corners", "ogr" ) - # Iterate over features and count those with slope_ang > 0.1 - feature_count_slope_ang_greater_than_0 = 0 + # Iterate over features and count those with slope_ang > 0.005 + feature_count_slope_ang_greater_than_005 = 0 for feature in output_wall_layer.getFeatures(): - if feature["slope_ang"] > 0: - feature_count_slope_ang_greater_than_0 += 1 + if feature["slope_ang"] > 0.005: + feature_count_slope_ang_greater_than_005 += 1 # Check if the counted features match the expected value expected_feature_wall_count = ( - 1273 # Hypothetical expected number of features with slope_ang > 0.1 + 61 # Hypothetical expected number of features with slope_ang > 0.005 ) self.assertEqual( - feature_count_slope_ang_greater_than_0, + feature_count_slope_ang_greater_than_005, expected_feature_wall_count, "The number of wall features with slope_ang > 0 does not match the expected count", ) @@ -276,17 +275,17 @@ def test_output_verification_with_all_params(self): ) # Iterate over features and count those with max_sv_tot > 0.1 - feature_count_max_sv_tot_greater_than_0 = 0 + feature_count_max_sv_tot_greater_than_005 = 0 for feature in output_building_layer.getFeatures(): - if feature["max_sv_tot"] > 0: - feature_count_max_sv_tot_greater_than_0 += 1 + if feature["max_sv_tot"] > 0.05: + feature_count_max_sv_tot_greater_than_005 += 1 # Example verification: Check if the counted features match the expected value expected_feature_building_count = ( - 188 # Hypothetical expected number of features with max_sv_tot > 0 + 176 # Hypothetical expected number of features with max_sv_tot > 0 ) self.assertEqual( - feature_count_max_sv_tot_greater_than_0, + feature_count_max_sv_tot_greater_than_005, expected_feature_building_count, "The number of building features with max_sv_tot > 0 does not match the expected count", ) diff --git a/geovita_processing_plugin/test/test_alg_impact.py b/geovita_processing_plugin/test/test_alg_impact.py index d6126d1..7819466 100644 --- a/geovita_processing_plugin/test/test_alg_impact.py +++ b/geovita_processing_plugin/test/test_alg_impact.py @@ -105,8 +105,7 @@ def setUp(self): "EXCAVATION_DEPTH": 10.0, "SETTLEMENT_ENUM": 1, # index "CLIPPING_RANGE": 150, - "POREPRESSURE_ENUM": 1, # index - "POREPRESSURE_REDUCTION": 50, + "POREWP_REDUCTION_M": 6, "DRY_CRUST_THICKNESS": 5.0, "DEPTH_GROUNDWATER": 3, "SOIL_DENSITY": 18.5, @@ -120,15 +119,11 @@ def setUp(self): # Define a dictionary of points to test with their expected raster values # Format: {"point_name": (x, y, expected_value)} self.test_points = { - "point1": ( - 429883.5, - 2323447.6, - 0.106683, - ), # Example coordinates and expected value - "point2": (429896.7, 2323504.7, 0.0896728), - "point3": (429865.0, 2323425.2, 0.111367), - "point4": (429864.0, 2323457.4, 0.0725033), - "point5": (429845.5, 2323466.2, 0.0308721), + "point1": (429880.4, 2323462.2, 0.138451), # Example coordinates and expected value + "point2": (429792.6, 2323390.1, 0.038517), + "point3": (429853.4, 2323441.1, 0.151161), + "point4": (429837.7, 2323561.8, 0.0321095), + "point5": (429972.6, 2323557.9, 0.0319429), } def test_algorithm_loaded(self): @@ -160,11 +155,11 @@ def test_algorithm_execution_all(self): # Verify results # For example, check if output shapefiles exist - self.assertTrue(Path(results["OUTPUT_FOLDER"]).exists()) + self.assertTrue(Path(results["OUTPUT_RASTER"]).exists()) # Further checks can include verifying the contents of the output shapefiles # Load the raster and perform additional checks - output_raster = QgsRasterLayer(results["OUTPUT_FOLDER"], "Output Raster") + output_raster = QgsRasterLayer(results["OUTPUT_RASTER"], "Output Raster") self.assertTrue(output_raster.isValid(), "Output raster layer is not valid.") def test_algorithm_exec_long(self): @@ -182,7 +177,7 @@ def test_algorithm_exec_long(self): # Verify results # For example, check if output shapefiles exist - self.assertTrue(Path(results["OUTPUT_FOLDER"]).exists()) + self.assertTrue(Path(results["OUTPUT_RASTER"]).exists()) # Further checks can include verifying the contents of the output shapefiles @@ -200,7 +195,7 @@ def test_output_raster_values_at_points(self): ) # Load the output layer for verification - output_layer = QgsRasterLayer(results["OUTPUT_FOLDER"], "Output Corners") + output_layer = QgsRasterLayer(results["OUTPUT_RASTER"], "Output Corners") for point_name, (x, y, expected_value) in self.test_points.items(): point = QgsPointXY(x, y) diff --git a/geovita_processing_plugin/test/test_alg_tunnel.py b/geovita_processing_plugin/test/test_alg_tunnel.py index ad052d1..d00fcef 100644 --- a/geovita_processing_plugin/test/test_alg_tunnel.py +++ b/geovita_processing_plugin/test/test_alg_tunnel.py @@ -110,7 +110,7 @@ def setUp(self): "RASTER_ROCK_SURFACE": self.raster_rock_surface_layer, "POREPRESSURE_ENUM": 1, # index "TUNNEL_LEAKAGE": 10, - "POREPRESSURE_REDUCTION": 50, # only used if POREPRESSURE_ENUM = 3 + "POREWP_REDUCTION_M": 10, # only used if POREPRESSURE_ENUM = 3 "DRY_CRUST_THICKNESS": 5.0, "DEPTH_GROUNDWATER": 3, "SOIL_DENSITY": 18.5, diff --git a/geovita_processing_plugin/utilities/AddLayersTask.py b/geovita_processing_plugin/utilities/AddLayersTask.py deleted file mode 100644 index 7433ce7..0000000 --- a/geovita_processing_plugin/utilities/AddLayersTask.py +++ /dev/null @@ -1,171 +0,0 @@ -from qgis.PyQt.QtCore import pyqtSignal -from qgis.core import (QgsTask, - QgsProject, - QgsRasterLayer, - QgsVectorLayer, - QgsLayerTreeGroup, - QgsLayerTreeLayer) - -from pathlib import Path -from datetime import datetime - -from geovita_processing_plugin.utilities import logger - -class AddLayersTask(QgsTask): - """ - A QGIS task for adding layers (vector or raster) to the QGIS interface. - This task handles the addition of layers in a background thread and updates - the GUI in the main thread upon completion. - - Attributes: - taskCompleted (pyqtSignal): Signal emitted when the task is completed. - layers_info (list[tuple[str, str, str]]): List of tuples containing layer information (name, path, style). - group_name (str): The name of the group to which layers are related. - styles_dir_path (Path): The directory path where style files are located. - logger (logging.Logger): Logger for logging messages. - prepared_layers (list[tuple[str, str, str, str]]): Prepared layer information for adding to QGIS. - completed (bool): Flag indicating whether the task has completed. - """ - - taskCompleted = pyqtSignal(bool) - - def __init__(self, description: str = "Add Layers", layers_info: dict = {}, group_name: str = None, style_dir_path: Path = None, logger: logger.CustomLogger = None) -> None: - """ - Initializes the AddLayersTask. - - Args: - description (str): The description of the task. - layers_info (dict): A dict containing layer information where the layer name is the key (Key: name, {path, style}). - group_name (str): The name of the group to which layers are related. - style_dir_path (Path): Path to styles directory - logger (Logger): Logger for logging messages. - """ - super().__init__("Add Layers Task", QgsTask.CanCancel) - self.layers_info = [] - self.group_name = "" - self.styles_dir_path = None - self.logger = None - self.prepared_layers = [] # Initialize prepared layers list - self.completed = False - - def setParameters(self, layers_info, group_name, style_dir_path, logger): - self.layers_info = layers_info - self.group_name = group_name - self.style_dir_path = style_dir_path - self.logger = logger - - def run(self) -> bool: - """ - Runs the task. The method that runs when the task is started. Used for non-GUI operations - such as data preparation and validation. - - Returns: - bool: True if preparation is successful, False otherwise. - """ - for layer_name, info in self.layers_info.items(): - layer_path = info["shape_path"] - style_name = info["style_name"] - # Validate file paths - if not Path(layer_path).is_file(): - self.logger.error(f"@AddLayersTask-run()@ - Layer file not found: {layer_path}") - return False - - # Generate a timestamp string - timestamp = datetime.now().strftime("_%Y%m%d_%H:%M") - modified_layer_name = f"{layer_name}{timestamp}" - - # Determine layer type without loading it - # add supported extensions - raster_ext = ('.tif', '.tiff') - # Determine layer type and create the layer object - if layer_path.endswith(raster_ext): - layer = QgsRasterLayer(layer_path, modified_layer_name) - else: - layer = QgsVectorLayer(layer_path, modified_layer_name, 'ogr') - - if not layer.isValid(): - self.logger.error(f"@AddLayersTask-run()@ - Failed to load layer: {layer_path}") - return False - - # Store the prepared layer along with its style name - self.prepared_layers.append((layer, style_name)) - self.logger.info(f"@AddLayersTask-run()@ - Layer prepared for addition: {modified_layer_name}") - return True - - def finished(self, success: bool) -> None: - """ - The method that runs when the task is finished. It is executed in the main thread, - making it safe for GUI operations like adding layers and refreshing the layer tree. - - Args: - success (bool): Indicates whether the task preparation was successful. - """ - if success: - # GUI operations are performed here - for layer, style_name in self.prepared_layers: - style_path = self.style_dir_path / style_name - if not style_path.is_file(): - self.logger.error(f"@AddLayersTask-run()@ - File not found: {style_path}") - if not self.add_layer_to_qgis(layer, str(style_path), self.group_name, self.logger): - self.logger.error(f"Failed to add layer {layer.name()}") - self.taskCompleted.emit(False) - return - - self.completed = True - self.taskCompleted.emit(success) - - def add_layer_to_qgis(self, layer: QgsRasterLayer or QgsVectorLayer, style_path: str, group_name: str = None, logger: logger.CustomLogger = None) -> bool: - """ - Adds a prepared layer to QGIS with a specified style. Optionally adds it to a specified group. - - Args: - layer (QgsRasterLayer or QgsVectorLayer): The prepared layer to be added. - style_path (str): Path to the QML style file. - group_name (str, optional): Name of the group to add the layer to. If None, adds without a group. - logger (logging.Logger, optional): Logger for logging messages. - - Returns: - bool: True if the layer is added successfully, False otherwise. - """ - # Apply the style and trigger refresh of layer - if Path(style_path).is_file(): - layer.loadNamedStyle(style_path) - layer.triggerRepaint() - - # Add the layer to the specified group or directly to the project - if group_name: - self._add_layer_to_group(layer, group_name, logger) - else: - QgsProject.instance().addMapLayer(layer, True) # True - add layer directly to the root - if logger: - logger.info(f"@add_layer_to_qgis@ - Layer '{layer.name()}' added to QGIS.") - - return True - - def _add_layer_to_group(self, layer: QgsRasterLayer or QgsVectorLayer, group_name: str, logger: logger.CustomLogger = None) -> None: - """ - Adds the specified layer to a group in the QGIS project. - - Args: - layer (QgsRasterLayer or QgsVectorLayer): The layer to add. - group_name (str): The name of the group. - logger (logging.Logger): Logger for logging messages. - """ - root = QgsProject.instance().layerTreeRoot() - group = root.findGroup(group_name) - if not group: - group = QgsLayerTreeGroup(group_name) - root.insertChildNode(0, group) - if logger: - logger.debug(f"@_add_layer_to_group@ - Created new group '{group_name}' and added it to the top of the Layer Tree.") - else: - if logger: - logger.debug(f"@_add_layer_to_group@ - Found existing group '{group_name}'.") - - QgsProject.instance().addMapLayer(layer, False) - node_layer = QgsLayerTreeLayer(layer) - group.addChildNode(node_layer) - # Log layer addition - if logger: - logger.debug(f"@_add_layer_to_group@ - Added layer '{layer.name()}' to group '{group_name}'.") -