diff --git a/arho_feature_template/core/feature_template_library.py b/arho_feature_template/core/feature_template_library.py index e6dc6e4..0dada5d 100644 --- a/arho_feature_template/core/feature_template_library.py +++ b/arho_feature_template/core/feature_template_library.py @@ -2,8 +2,8 @@ import logging from collections import defaultdict +from typing import TYPE_CHECKING -from qgis.core import QgsFeature, QgsProject, QgsVectorLayer from qgis.gui import QgsMapToolDigitizeFeature from qgis.PyQt.QtCore import QItemSelectionModel from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel @@ -16,40 +16,16 @@ TemplateSyntaxError, parse_template_library_config, ) +from arho_feature_template.exceptions import LayerNotFoundError, LayerNotVectorTypeError from arho_feature_template.gui.template_attribute_form import TemplateAttributeForm from arho_feature_template.gui.template_dock import TemplateLibraryDock from arho_feature_template.resources.template_libraries import library_config_files +from arho_feature_template.utils.project_utils import get_layer_from_project -logger = logging.getLogger(__name__) - - -class LayerNotFoundError(Exception): - def __init__(self, layer_name: str): - super().__init__(f"Layer {layer_name} not found") - - -class LayerNotVectorTypeError(Exception): - def __init__(self, layer_name: str): - super().__init__(f"Layer {layer_name} is not a vector layer") - +if TYPE_CHECKING: + from qgis.core import QgsFeature, QgsVectorLayer -def get_layer_from_project(layer_name: str) -> QgsVectorLayer: - project = QgsProject.instance() - if not project: - raise LayerNotFoundError(layer_name) - - layers = project.mapLayersByName(layer_name) - if not layers: - raise LayerNotFoundError(layer_name) - - if len(layers) > 1: - logger.warning("Multiple layers with the same name found. Using the first one.") - - layer = layers[0] - if not isinstance(layer, QgsVectorLayer): - raise LayerNotVectorTypeError(layer_name) - - return layer +logger = logging.getLogger(__name__) class TemplateItem(QStandardItem): diff --git a/arho_feature_template/core/plan_manager.py b/arho_feature_template/core/plan_manager.py index 19ac597..6b8a137 100644 --- a/arho_feature_template/core/plan_manager.py +++ b/arho_feature_template/core/plan_manager.py @@ -1,18 +1,83 @@ from __future__ import annotations import json +import logging +from string import Template +from textwrap import dedent from qgis.core import QgsExpressionContextUtils, QgsProject, QgsVectorLayer from qgis.PyQt.QtWidgets import QDialog, QMessageBox from qgis.utils import iface from arho_feature_template.core.lambda_service import LambdaService -from arho_feature_template.core.update_plan import LandUsePlan, update_selected_plan from arho_feature_template.gui.load_plan_dialog import LoadPlanDialog from arho_feature_template.gui.serialize_plan import SerializePlan from arho_feature_template.utils.db_utils import get_existing_database_connection_names from arho_feature_template.utils.misc_utils import get_active_plan_id, get_layer_by_name, handle_unsaved_changes +logger = logging.getLogger(__name__) + +LAYER_NAME_PLAN = "Kaava" +LAYER_NAME_LAND_USE_POINT = "Maankäytön kohteet" +LAYER_NAME_OTHER_POINT = "Muut pisteet" +LAYER_NAME_LINE = "Viivat" +LAYER_NAME_OTHER_AREA = "Osa-alue" +LAYER_NAME_LAND_USE_AREA = "Aluevaraus" +LAYER_NAME_PLAN_REGULATION_GROUP = "Kaavamääräysryhmät" +LAYER_NAME_PLAN_REGULATION = "Kaavamääräys" +LAYER_NAME_PLAN_PROPOSITION = "Kaavasuositus" +LAYER_NAME_DOCUMENT = "Asiakirjat" +LAYER_NAME_SOURCE_DATA = "Lähtötietoaineistot" +LAYER_NAME_REGULATION_GROUP_ASSOCIATION = "Kaavamääräysryhmien assosiaatiot" + +PLAN_FILTER_TEMPLATES = { + LAYER_NAME_PLAN: Template("id = '$plan_id'"), + LAYER_NAME_LAND_USE_POINT: Template("plan_id = '$plan_id'"), + LAYER_NAME_OTHER_POINT: Template("plan_id = '$plan_id'"), + LAYER_NAME_LINE: Template("plan_id = '$plan_id'"), + LAYER_NAME_LAND_USE_AREA: Template("plan_id = '$plan_id'"), + LAYER_NAME_OTHER_AREA: Template("plan_id = '$plan_id'"), + LAYER_NAME_PLAN_REGULATION_GROUP: Template("plan_id = '$plan_id'"), + LAYER_NAME_REGULATION_GROUP_ASSOCIATION: Template( + dedent( + """\ + EXISTS ( + SELECT 1 + FROM hame.plan_regulation_group prg + WHERE + hame.regulation_group_association.plan_regulation_group_id = prg.id + AND prg.plan_id = '$plan_id' + )""" + ) + ), + LAYER_NAME_PLAN_REGULATION: Template( + dedent( + """\ + EXISTS ( + SELECT 1 + FROM hame.plan_regulation_group prg + WHERE + hame.plan_regulation.plan_regulation_group_id = prg.id + AND prg.plan_id = '$plan_id' + )""" + ) + ), + LAYER_NAME_PLAN_PROPOSITION: Template( + dedent( + """\ + EXISTS ( + SELECT 1 + FROM hame.plan_regulation_group rg + WHERE + hame.plan_proposition.plan_regulation_group_id = rg.id + AND rg.plan_id = '$plan_id' + )""" + ) + ), + LAYER_NAME_DOCUMENT: Template("plan_id = '$plan_id'"), + LAYER_NAME_SOURCE_DATA: Template("plan_id = '$plan_id'"), +} + class PlanManager: def __init__(self): @@ -24,11 +89,11 @@ def add_new_plan(self): if not handle_unsaved_changes(): return - plan_layer = get_layer_by_name("Kaava") - self.clear_all_filters() + plan_layer = get_layer_by_name(LAYER_NAME_PLAN) if not plan_layer: return + self.set_active_plan(None) if not plan_layer.isEditable(): plan_layer.startEditing() @@ -37,17 +102,16 @@ def add_new_plan(self): iface.actionAddFeature().trigger() # Connect the featureAdded signal to a callback method - plan_layer.featureAdded.connect(self.feature_added) + plan_layer.featureAdded.connect(self._feature_added) - def feature_added(self): + def _feature_added(self): """Callback for when a new feature is added to the Kaava layer.""" - - plan_layer = get_layer_by_name("Kaava") + plan_layer = get_layer_by_name(LAYER_NAME_PLAN) if not plan_layer: return # Disconnect the signal to avoid repeated triggers - plan_layer.featureAdded.disconnect() + plan_layer.featureAdded.disconnect(self._feature_added) feature_ids_before_commit = plan_layer.allFeatureIds() @@ -60,18 +124,40 @@ def feature_added(self): return feature_ids_after_commit = plan_layer.allFeatureIds() - new_feature_id = next((fid for fid in feature_ids_after_commit if fid not in feature_ids_before_commit), None) + new_feature_id = next( + (fid for fid in feature_ids_after_commit if fid not in feature_ids_before_commit), + None, + ) if new_feature_id is not None: new_feature = plan_layer.getFeature(new_feature_id) if new_feature.isValid(): feature_id_value = new_feature["id"] - update_selected_plan(LandUsePlan(feature_id_value)) + self.set_active_plan(feature_id_value) else: iface.messageBar().pushMessage("Error", "Invalid feature retrieved.", level=3) else: iface.messageBar().pushMessage("Error", "No new feature was added.", level=3) + def set_active_plan(self, plan_id: str | None): + """Update the project layers based on the selected land use plan.""" + QgsExpressionContextUtils.setProjectVariable(QgsProject.instance(), "active_plan_id", plan_id) + + for layer_name, filter_template in PLAN_FILTER_TEMPLATES.items(): + """Set a filter for the given vector layer.""" + filter_expression = filter_template.substitute(plan_id=plan_id) if plan_id else None + layer = get_layer_by_name(layer_name) + if not layer: + logger.warning("Layer %s not found", layer_name) + continue + result = layer.setSubsetString(filter_expression) + if result is False: + iface.messageBar().pushMessage( + "Error", + f"Failed to filter layer {layer_name} with query {filter_expression}", + level=3, + ) + def load_land_use_plan(self): """Load an existing land use plan using a dialog selection.""" connections = get_existing_database_connection_names() @@ -93,15 +179,7 @@ def load_land_use_plan(self): QMessageBox.critical(None, "Error", "No plan was selected.") return - plan = LandUsePlan(selected_plan_id) - update_selected_plan(plan) - - def clear_all_filters(self): - """Clear active_plan_id and filters for all vector layers in the project.""" - QgsExpressionContextUtils.setProjectVariable(QgsProject.instance(), "active_plan_id", None) - for layer in QgsProject.instance().mapLayers().values(): - if isinstance(layer, QgsVectorLayer): - layer.setSubsetString("") + self.set_active_plan(selected_plan_id) def commit_all_editable_layers(self): """Commit all changes in any editable layers.""" diff --git a/arho_feature_template/core/update_plan.py b/arho_feature_template/core/update_plan.py deleted file mode 100644 index 8552fc7..0000000 --- a/arho_feature_template/core/update_plan.py +++ /dev/null @@ -1,63 +0,0 @@ -from dataclasses import dataclass - -from qgis.core import QgsExpressionContextUtils, QgsMapLayer, QgsProject, QgsVectorLayer -from qgis.utils import iface - - -# To be extended and moved -@dataclass -class LandUsePlan: - id: str - - -# To be replaced later -LAYER_PLAN_ID_MAP = { - "Kaava": "id", - "Maankäytön kohteet": "plan_id", - "Muut pisteet": "plan_id", - "Viivat": "plan_id", - "Aluevaraus": "plan_id", - "Osa-alue": "plan_id", -} - - -def update_selected_plan(new_plan: LandUsePlan): - """Update the project layers based on the selected land use plan.""" - plan_id = new_plan.id - QgsExpressionContextUtils.setProjectVariable(QgsProject.instance(), "active_plan_id", plan_id) - - for layer_name, field_name in LAYER_PLAN_ID_MAP.items(): - # Set the filter on each layer using the plan_id - set_filter_for_vector_layer(layer_name, field_name, plan_id) - - -def set_filter_for_vector_layer(layer_name: str, field_name: str, field_value: str): - """Set a filter for the given vector layer.""" - layers = QgsProject.instance().mapLayersByName(layer_name) - - if not _check_layer_count(layers): - return - - layer = layers[0] - - expression = f"\"{field_name}\" = '{field_value}'" - - # Apply the filter to the layer - if not layer.setSubsetString(expression): - iface.messageBar().pushMessage("Error", f"Failed to filter layer {layer_name} with query {expression}", level=3) - - -def _check_layer_count(layers: list) -> bool: - """Check if any layers are returned.""" - if not layers: - iface.messageBar().pushMessage("Error", "ERROR: No layers found with the specified name.", level=3) - return False - return True - - -def _check_vector_layer(layer: QgsMapLayer) -> bool: - """Check if the given layer is a vector layer.""" - if not isinstance(layer, QgsVectorLayer): - iface.messageBar().pushMessage("Error", f"Layer {layer.name()} is not a vector layer: {type(layer)}", level=3) - return False - return True diff --git a/arho_feature_template/exceptions.py b/arho_feature_template/exceptions.py new file mode 100644 index 0000000..51350d7 --- /dev/null +++ b/arho_feature_template/exceptions.py @@ -0,0 +1,8 @@ +class LayerNotFoundError(Exception): + def __init__(self, layer_name: str): + super().__init__(f"Layer {layer_name} not found") + + +class LayerNotVectorTypeError(Exception): + def __init__(self, layer_name: str): + super().__init__(f"Layer {layer_name} is not a vector layer") diff --git a/arho_feature_template/utils/project_utils.py b/arho_feature_template/utils/project_utils.py new file mode 100644 index 0000000..e3ba401 --- /dev/null +++ b/arho_feature_template/utils/project_utils.py @@ -0,0 +1,26 @@ +import logging + +from qgis.core import QgsProject, QgsVectorLayer + +from arho_feature_template.exceptions import LayerNotFoundError, LayerNotVectorTypeError + +logger = logging.getLogger(__name__) + + +def get_layer_from_project(layer_name: str) -> QgsVectorLayer: + project = QgsProject.instance() + if not project: + raise LayerNotFoundError(layer_name) + + layers = project.mapLayersByName(layer_name) + if not layers: + raise LayerNotFoundError(layer_name) + + if len(layers) > 1: + logger.warning("Multiple layers with the same name found. Using the first one.") + + layer = layers[0] + if not isinstance(layer, QgsVectorLayer): + raise LayerNotVectorTypeError(layer_name) + + return layer diff --git a/qgisprojekti.qgz b/qgisprojekti.qgz new file mode 100644 index 0000000..11c6d75 Binary files /dev/null and b/qgisprojekti.qgz differ