Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lisää Tuo kaavakohteita -lomake #225

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions arho_feature_template/core/plan_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
RegulationGroupLibrary,
)
from arho_feature_template.exceptions import UnsavedChangesError
from arho_feature_template.gui.dialogs.import_features_form import ImportFeaturesForm
from arho_feature_template.gui.dialogs.lifecycle_editor import LifecycleEditor
from arho_feature_template.gui.dialogs.load_plan_dialog import LoadPlanDialog
from arho_feature_template.gui.dialogs.plan_attribute_form import PlanAttributeForm
Expand All @@ -33,16 +34,11 @@
from arho_feature_template.gui.tools.inspect_plan_features_tool import InspectPlanFeatures
from arho_feature_template.project.layers.code_layers import PlanRegulationGroupTypeLayer, code_layers
from arho_feature_template.project.layers.plan_layers import (
FEATURE_LAYER_NAME_TO_CLASS_MAP,
AdditionalInformationLayer,
DocumentLayer,
LandUseAreaLayer,
LandUsePointLayer,
LegalEffectAssociationLayer,
LifeCycleLayer,
LineLayer,
OtherAreaLayer,
OtherPointLayer,
PlanFeatureLayer,
PlanLayer,
PlanPropositionLayer,
PlanRegulationLayer,
Expand Down Expand Up @@ -72,14 +68,6 @@

logger = logging.getLogger(__name__)

FEATURE_LAYER_NAME_TO_CLASS_MAP: dict[str, type[PlanFeatureLayer]] = {
LandUsePointLayer.name: LandUsePointLayer,
OtherPointLayer.name: OtherPointLayer,
LineLayer.name: LineLayer,
OtherAreaLayer.name: OtherAreaLayer,
LandUseAreaLayer.name: LandUseAreaLayer,
}


class PlanDigitizeMapTool(QgsMapToolDigitizeFeature): ...

Expand Down Expand Up @@ -158,6 +146,11 @@ def initialize_libraries(self):
]
self.new_feature_dock.initialize_feature_template_libraries(self.feature_template_libraries)

def open_import_features_dialog(self):
import_features_form = ImportFeaturesForm(self.active_plan_regulation_group_library)
if import_features_form.exec_():
pass

def update_active_plan_regulation_group_library(self):
self.active_plan_regulation_group_library = regulation_group_library_from_active_plan()
self.regulation_groups_dock.update_regulation_groups(self.active_plan_regulation_group_library)
Expand Down
228 changes: 228 additions & 0 deletions arho_feature_template/gui/dialogs/import_features_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
from __future__ import annotations

from importlib import resources
from typing import TYPE_CHECKING

from qgis.core import (
QgsFeature,
QgsFeatureIterator,
QgsFeatureRequest,
QgsFieldProxyModel,
QgsVectorLayer,
)
from qgis.PyQt import uic
from qgis.PyQt.QtWidgets import QCheckBox, QDialog, QDialogButtonBox, QProgressBar

from arho_feature_template.core.models import PlanFeature, RegulationGroupLibrary
from arho_feature_template.project.layers.code_layers import UndergroundTypeLayer, code_layers
from arho_feature_template.project.layers.plan_layers import (
FEATURE_LAYER_NAME_TO_CLASS_MAP,
RegulationGroupAssociationLayer,
plan_feature_layers,
plan_layers,
)
from arho_feature_template.utils.misc_utils import iface, use_wait_cursor

if TYPE_CHECKING:
from qgis.gui import QgsCheckableComboBox, QgsFieldComboBox, QgsFieldExpressionWidget, QgsMapLayerComboBox

from arho_feature_template.gui.components.code_combobox import CodeComboBox


ui_path = resources.files(__package__) / "import_features_form.ui"
FormClass, _ = uic.loadUiType(ui_path)


class ImportFeaturesForm(QDialog, FormClass):
def __init__(self, active_plan_regulation_groups_library: RegulationGroupLibrary):
super().__init__()
self.setupUi(self)

# TYPES
self.source_layer_selection: QgsMapLayerComboBox
self.filter_expression: QgsFieldExpressionWidget
self.selected_features_only: QCheckBox

self.name_selection: QgsFieldComboBox
self.description_selection: QgsFieldComboBox
self.feature_type_of_underground_selection: CodeComboBox
self.regulation_groups_selection: QgsCheckableComboBox

self.target_layer_selection: QgsMapLayerComboBox

self.progress_bar: QProgressBar
self.process_button_box: QDialogButtonBox

# INIT
self.process_button_box.button(QDialogButtonBox.Ok).setText("Import")
self.process_button_box.accepted.connect(self.import_features)
self.process_button_box.rejected.connect(self.reject)

# Source layer initialization
# Exclude all project layers from valid source layers
# NOTE: Some project layers are not included in either `plan_layers` or `code_layers`?
self.source_layer_selection.setLayer(iface.activeLayer())
excluded_layers = [layer.get_from_project() for layer in plan_layers + code_layers]
self.source_layer_selection.setExceptedLayerList(excluded_layers)
self.source_layer_selection.layerChanged.connect(self._on_layer_selections_changed)

# Target layer initialization
# Set only plan feature layers as valid target layers
self.target_layer_selection.clear()
self.target_layer_selection.setAdditionalLayers(layer.get_from_project() for layer in plan_feature_layers)
self.target_layer_selection.setCurrentIndex(0)
self.target_layer_selection.layerChanged.connect(self._on_layer_selections_changed)

# Name field initialization
self.name_selection.setAllowEmptyFieldName(True)
self.name_selection.setFilters(QgsFieldProxyModel.Filter.String)
self.name_selection.setField("")

# Description field initialization
self.description_selection.setAllowEmptyFieldName(True)
self.description_selection.setFilters(QgsFieldProxyModel.Filter.String)
self.description_selection.setField("")

# Underground type initialization
# Remove NULL from the selections and set Maanpäällinen as default
self.feature_type_of_underground_selection.populate_from_code_layer(UndergroundTypeLayer)
self.feature_type_of_underground_selection.remove_item_by_text("NULL")
self.feature_type_of_underground_selection.setCurrentIndex(1) # Set default to Maanpäällinen (index 1)

# Regulation groups initialization
# Only regulation group already in DB are shown and they are not categorized right now
# NOTE: This means groups that are "Aluevaraus" groups can be given to "Osa-alue" for example
i = 0
for category in active_plan_regulation_groups_library.regulation_group_categories:
for group in category.regulation_groups:
self.regulation_groups_selection.addItem(str(group))
self.regulation_groups_selection.setItemData(i, group.id_)
i += 1

self._on_layer_selections_changed(self.source_layer_selection.currentLayer())

def _on_layer_selections_changed(self, _: QgsVectorLayer):
self.source_layer: QgsVectorLayer = self.source_layer_selection.currentLayer()
self.source_layer_name: str = self.source_layer.name()
self.target_layer: QgsVectorLayer = self.target_layer_selection.currentLayer()
self.target_layer_name: str = self.target_layer.name()

self.filter_expression.setLayer(self.source_layer)
self.name_selection.setLayer(self.source_layer)
self.description_selection.setLayer(self.source_layer)

if self.source_and_target_layer_types_match():
self.process_button_box.button(QDialogButtonBox.Ok).setEnabled(True)
else:
self.process_button_box.button(QDialogButtonBox.Ok).setEnabled(False)

def source_and_target_layer_types_match(self) -> bool:
if not self.source_layer or not self.target_layer:
return False
return self.source_layer.wkbType() is self.target_layer.wkbType()

@use_wait_cursor
def import_features(self):
if not self.source_layer or not self.target_layer:
return

self.progress_bar.setValue(0)
source_features = list(self.get_source_features(self.source_layer))
if not source_features:
iface.messageBar().pushInfo("", "Yhtään kohdetta ei tuotu.")
return

# Create and add new plan features
plan_features = self.create_plan_features(source_features)
total_count = len(plan_features)
failed_count = 0
success_count = 0
for i, feat in enumerate(plan_features):
self.progress_bar.setValue(int((i + 1) / total_count * 100))
if self._save_feature(feat, self.target_layer, None, "Kaavakohteen lisääminen"):
success_count += 1
else:
failed_count += 1

# If regulation groups are defined, associate them with the plan features
if len(self.regulation_groups_selection.checkedItems()) > 0:
associations = self.create_regulation_group_associations(plan_features)
associations_layer = RegulationGroupAssociationLayer.get_from_project()
for association in associations:
self._save_feature(association, associations_layer, None, "Kaavamääräysryhmän assosiaation lisääminen")

if failed_count == 0:
iface.messageBar().pushSuccess("", "Kaavakohteet tuotiin onnistuneesti.")
else:
iface.messageBar().pushInfo("", f"Osa kaavakohteista tuotiin epäonnistuneesti ({failed_count}).")

self.progress_bar.setValue(100)

def get_source_features(self, source_layer: QgsVectorLayer) -> QgsFeatureIterator | list[QgsFeature]:
expression_text = self.filter_expression.currentText()

# Case 1: Both selection and expression
if self.selected_features_only.isChecked() and expression_text:
selected_features = source_layer.selectedFeatures()
request = QgsFeatureRequest().setFilterExpression(expression_text)
source_features = [feat for feat in source_layer.getFeatures(request) if feat in selected_features]

# Case 2: Only selection
elif self.selected_features_only.isChecked():
source_features = source_layer.selectedFeatures()

# Case 3: Only expression
elif expression_text:
request = QgsFeatureRequest().setFilterExpression(expression_text)
source_features = source_layer.getFeatures(request)

# Case 4: No expression or selection
else:
source_features = source_layer.getFeatures()

return source_features

def create_plan_features(self, source_features: QgsFeatureIterator | list[QgsFeature]) -> list[QgsFeature]:
type_of_underground_id = self.feature_type_of_underground_selection.value()
source_layer_name_field = self.name_selection.currentField()
source_layer_description_field = self.description_selection.currentField()
layer_class = FEATURE_LAYER_NAME_TO_CLASS_MAP.get(self.target_layer_name)
if not layer_class:
msg = f"Could not find plan feature layer class for layer name {self.target_layer_name}"
raise ValueError(msg)

return [
layer_class.feature_from_model(
PlanFeature(
geom=feature.geometry(),
type_of_underground_id=type_of_underground_id,
layer_name=self.target_layer_name,
name=feature[source_layer_name_field] if source_layer_name_field else None,
description=feature[source_layer_description_field] if source_layer_description_field else None,
)
)
for feature in source_features
]

def create_regulation_group_associations(self, plan_features: list[QgsFeature]) -> list[QgsFeature]:
return [
RegulationGroupAssociationLayer.feature_from(
regulation_group_id, self.target_layer_name, plan_feature["id"]
)
for regulation_group_id in self.regulation_groups_selection.checkedItemsData()
for plan_feature in plan_features
]

@staticmethod
def _save_feature(feature: QgsFeature, layer: QgsVectorLayer, id_: str | None, edit_text: str = "") -> bool:
if not layer.isEditable():
layer.startEditing()
layer.beginEditCommand(edit_text)

if id_ is None:
layer.addFeature(feature)
else:
layer.updateFeature(feature)

layer.endEditCommand()
return layer.commitChanges(stopEditing=False)
Loading
Loading