diff --git a/arho_feature_template/core/feature_template_library.py b/arho_feature_template/core/feature_template_library.py
index ebfaed6..0e37cb1 100644
--- a/arho_feature_template/core/feature_template_library.py
+++ b/arho_feature_template/core/feature_template_library.py
@@ -9,6 +9,7 @@
from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel
from qgis.utils import iface
+from arho_feature_template.core.plan_manager import save_plan_feature
from arho_feature_template.core.template_library_config import (
FeatureTemplate,
TemplateLibraryConfig,
@@ -157,16 +158,9 @@ def ask_for_feature_attributes(self, feature: QgsFeature) -> None:
if not self.active_template:
return
- attribute_form = TemplateAttributeForm(self.active_template.config)
-
+ attribute_form = TemplateAttributeForm(self.active_template.config, feature.geometry())
if attribute_form.exec_():
- layer = get_vector_layer_from_project(self.active_template.config.feature.layer)
- # Save the feature
- attribute_form.set_feature_attributes(feature)
-
- layer.beginEditCommand("Create feature from template")
- layer.addFeature(feature)
- layer.commitChanges(stopEditing=False)
+ save_plan_feature(attribute_form.model)
def get_library_names(self) -> list[str]:
return list(self.library_configs.keys())
diff --git a/arho_feature_template/core/models.py b/arho_feature_template/core/models.py
index 160630e..dcfaed3 100644
--- a/arho_feature_template/core/models.py
+++ b/arho_feature_template/core/models.py
@@ -1,28 +1,285 @@
from __future__ import annotations
+import logging
+import os
from dataclasses import dataclass, field
+from enum import Enum
+from pathlib import Path
from typing import TYPE_CHECKING
+import yaml
+
+from arho_feature_template.exceptions import ConfigSyntaxError
+from arho_feature_template.qgis_plugin_tools.tools.resources import resources_path
+from arho_feature_template.utils.misc_utils import get_layer_by_name, iface
+
if TYPE_CHECKING:
- from qgis.core import QgsGeometry
+ from typing import Literal
+
+ from qgis.core import QgsFeature, QgsGeometry
+
+
+logger = logging.getLogger(__name__)
+
+DEFAULT_PLAN_REGULATIONS_CONFIG_PATH = Path(os.path.join(resources_path(), "kaavamaaraykset.yaml"))
+
+
+class ValueType(Enum):
+ DECIMAL = "desimaali"
+ POSITIVE_DECIMAL = "positiivinen desimaali"
+ POSITIVE_INTEGER = "positiivinen kokonaisluku"
+ POSITIVE_INTEGER_RANGE = "positiivinen kokonaisluku arvoväli"
+ VERSIONED_TEXT = "kieliversioitu teksti"
+
+
+@dataclass
+class RegulationGroupCategory:
+ category_code: str
+ name: str | None
+ regulation_groups: list[RegulationGroup]
+
+ @classmethod
+ def from_config_data(cls, data: dict) -> RegulationGroupCategory:
+ return cls(
+ category_code=data["category_code"],
+ name=data.get("name"),
+ regulation_groups=[
+ RegulationGroup.from_config_data(config_data) for config_data in data["plan_regulation_groups"]
+ ],
+ )
+
+
+@dataclass
+class RegulationGroupLibrary:
+ """Describes the configuration of a plan regulation group library"""
+
+ name: str
+ version: int | None
+ description: str | None
+ regulation_group_categories: list[RegulationGroupCategory]
+ # regulation_groups: list[RegulationGroup]
+
+ @classmethod
+ def from_config_file(cls, config_fp: Path) -> RegulationGroupLibrary:
+ with config_fp.open(encoding="utf-8") as f:
+ data = yaml.safe_load(f)
+ return RegulationGroupLibrary(
+ name=data["name"],
+ version=data.get("version"),
+ description=data.get("description"),
+ regulation_group_categories=[
+ RegulationGroupCategory.from_config_data(category) for category in data["categories"]
+ ],
+ )
+
+
+@dataclass
+class RegulationLibrary:
+ """Describes the set of plan regulations."""
+
+ version: str
+ regulations: list[RegulationConfig]
+ regulations_dict: dict[str, RegulationConfig]
+
+ _instance: RegulationLibrary | None = None
+
+ @classmethod
+ def get_instance(cls) -> RegulationLibrary:
+ """Get the singleton instance, if initialized."""
+ if cls._instance is None:
+ return cls.initialize()
+ return cls._instance
+
+ @classmethod
+ def get_regulations(cls) -> list[RegulationConfig]:
+ """Get the list of top-level regulation configs, if instance is initialized."""
+ return cls.get_instance().regulations
+
+ @classmethod
+ def get_regulations_dict(cls) -> dict[str, RegulationConfig]:
+ """Get all regulations in a dictionary where keys are regulations codes and values RegulationConfigs."""
+ return cls.get_instance().regulations_dict
+
+ @classmethod
+ def get_regulation_by_code(cls, regulation_code: str) -> RegulationConfig | None:
+ """Get a regulation by it's regulation code (if exists)."""
+ return cls.get_instance().regulations_dict.get(regulation_code)
+
+ @classmethod
+ def initialize(
+ cls,
+ config_path: Path = DEFAULT_PLAN_REGULATIONS_CONFIG_PATH,
+ type_of_plan_regulations_layer_name="Kaavamääräyslaji",
+ language: Literal["fin", "eng", "swe"] = "fin",
+ ) -> RegulationLibrary:
+ # Initialize RegulationLibrary and RegulationConfigs from QGIS layer and config file
+
+ # 1. Read config file into a dict
+ with config_path.open(encoding="utf-8") as f:
+ config_data = yaml.safe_load(f)
+
+ # 2. Read code layer
+ layer = get_layer_by_name(type_of_plan_regulations_layer_name)
+ if layer is None:
+ msg = f"Could not find layer {type_of_plan_regulations_layer_name}!"
+ raise KeyError(msg)
+
+ # 3. Initialize regulation configs from layer. Storing them by their ID is handy for adding childs later
+ id_to_regulation_map: dict[str, RegulationConfig] = {
+ feature["id"]: RegulationConfig.from_feature(feature, language) for feature in layer.getFeatures()
+ }
+
+ # 4. Add information from config file (value, unit, category only) and link child regulations
+ try:
+ regulation_data: dict = {data["regulation_code"]: data for data in config_data["plan_regulations"]}
+ top_level_regulations: list[RegulationConfig] = []
+ for regulation_config in id_to_regulation_map.values():
+ # Add possible information from config data file
+ data = regulation_data.get(regulation_config.regulation_code)
+ if data:
+ regulation_config.category_only = data.get("category_only", False)
+ regulation_config.value_type = ValueType(data["value_type"]) if "value_type" in data else None
+ regulation_config.unit = data["unit"] if "unit" in data else None
+
+ # Top-level, add to list
+ if not regulation_config.parent_id:
+ top_level_regulations.append(regulation_config)
+ else:
+ # Add as child of another regulation
+ id_to_regulation_map[regulation_config.parent_id].child_regulations.append(regulation_config)
+ except KeyError as e:
+ raise ConfigSyntaxError(str(e)) from e
+
+ # 5. Create dictionary, useful when creating PlanRegulationDefinitions at least
+ regulations_dict: dict[str, RegulationConfig] = {}
+ for reg in top_level_regulations:
+ reg.add_to_dictionary(regulations_dict)
+
+ # 5. Create instance
+ cls._instance = cls(
+ version=config_data["version"], regulations=top_level_regulations, regulations_dict=regulations_dict
+ )
+ logger.info("RegulationLibrary initialized successfully.")
+ return cls._instance
+
+
+@dataclass
+class RegulationConfig:
+ """
+ Describes plan regulation type.
+
+ Initialized from DB/QGIS layer and extended with data from a config file.
+ """
+
+ id: str
+ regulation_code: str
+ name: str
+ description: str
+ status: str
+ level: int
+ parent_id: str | None
+ child_regulations: list[RegulationConfig] = field(default_factory=list)
+
+ # Data from config file
+ category_only: bool = False
+ value_type: ValueType | None = None
+ unit: str | None = None
+
+ # NOTE: Perhaps this ("model_from_feature") should be method of PlanTypeLayer class?
+ @classmethod
+ def from_feature(cls, feature: QgsFeature, language: str = "fin") -> RegulationConfig:
+ """
+ Initialize PlanRegulationConfig from QgsFeature.
+
+ Child regulations, value type ,category only and unit need to be set separately.
+ """
+ return cls(
+ id=feature["id"],
+ regulation_code=feature["value"],
+ name=feature["name"][language],
+ description=feature["description"][language],
+ status=feature["status"],
+ level=feature["level"],
+ parent_id=feature["parent_id"],
+ )
+
+ def add_to_dictionary(self, dictionary: dict[str, RegulationConfig]):
+ """Add child regulations to dictionary too."""
+ dictionary[self.regulation_code] = self
+ for regulation in self.child_regulations:
+ regulation.add_to_dictionary(dictionary)
@dataclass
class Regulation:
- type_code: str
- value_string: str | None
- value_int: int | None
- value_float: float | None
- unit: str | None
+ config: RegulationConfig # includes regulation_code and unit among other needed data for saving feature
+ value: str | float | int | tuple[int, int] | None = None
+ additional_information: dict[str, str | float | int | None] | None = None
+ regulation_number: int | None = None
+ files: list[str] = field(default_factory=list)
+ theme: str | None = None
+ topic_tag: str | None = None
+ regulation_group_id_: int | None = None
id_: int | None = None
+ # value_string: str | None
+ # value_number: Number | None
+ # value_number_pair: tuple[Number, Number] | None
@dataclass
class RegulationGroup:
- name: str
+ type_code_id: str | None
+ name: str | None
short_name: str | None
- type_code: str
- regulations: list[Regulation]
+ color_code: str | None
+ regulations: list[Regulation] = field(default_factory=list)
+ id_: int | None = None
+
+ @classmethod
+ def from_config_data(cls, data: dict) -> RegulationGroup:
+ regulations = []
+ for reg_data in data["plan_regulations"]:
+ reg_code = reg_data["regulation_code"]
+ config = RegulationLibrary.get_regulation_by_code(reg_code)
+ if config:
+ info_data = reg_data.get("additional_information")
+ regulations.append(
+ Regulation(
+ config=config,
+ value=reg_data.get("value"),
+ additional_information={info["type"]: info.get("value") for info in info_data}
+ if info_data
+ else None,
+ regulation_number=reg_data.get("regulation_number"),
+ files=reg_data.get("files") if reg_data.get("files") else [],
+ theme=reg_data.get("theme"),
+ topic_tag=reg_data.get("topic_tag"),
+ regulation_group_id_=None,
+ id_=None,
+ )
+ )
+ else:
+ iface.messageBar().pushWarning("", f"Could not find plan regulation {reg_code}!")
+ return cls(
+ type_code_id=data.get("type"), # NOTE: Might need to convert type code into type code ID here when
+ # config file has type codes for regulation groups
+ name=data.get("name"),
+ short_name=data.get("short_name"),
+ color_code=data.get("color_code"),
+ regulations=regulations,
+ id_=None,
+ )
+
+
+@dataclass
+class PlanFeature:
+ geom: QgsGeometry
+ type_of_underground_id: str
+ layer_name: str | None = None
+ name: str | None = None
+ description: str | None = None
+ regulation_groups: list[RegulationGroup] = field(default_factory=list)
+ plan_id: int | None = None
id_: int | None = None
diff --git a/arho_feature_template/core/plan_manager.py b/arho_feature_template/core/plan_manager.py
index 9e4f907..71f91c5 100644
--- a/arho_feature_template/core/plan_manager.py
+++ b/arho_feature_template/core/plan_manager.py
@@ -18,7 +18,19 @@
from arho_feature_template.gui.load_plan_dialog import LoadPlanDialog
from arho_feature_template.gui.plan_attribure_form import PlanAttributeForm
from arho_feature_template.gui.serialize_plan import SerializePlan
-from arho_feature_template.project.layers.plan_layers import PlanLayer, RegulationGroupLayer, plan_layers
+from arho_feature_template.project.layers.plan_layers import (
+ LandUseAreaLayer,
+ LandUsePointLayer,
+ LineLayer,
+ OtherAreaLayer,
+ OtherPointLayer,
+ PlanFeatureLayer,
+ PlanLayer,
+ PlanRegulationLayer,
+ RegulationGroupAssociationLayer,
+ RegulationGroupLayer,
+ plan_layers,
+)
from arho_feature_template.utils.db_utils import get_existing_database_connection_names
from arho_feature_template.utils.misc_utils import (
check_layer_changes,
@@ -29,7 +41,8 @@
if TYPE_CHECKING:
from qgis.core import QgsFeature
- from arho_feature_template.core.models import Plan, RegulationGroup
+ from arho_feature_template.core.models import Plan, PlanFeature, Regulation, RegulationGroup
+
logger = logging.getLogger(__name__)
@@ -179,6 +192,20 @@ def save_plan_jsons(self, plan_json, outline_json):
)
+def _save_feature(feature: QgsFeature, layer: QgsVectorLayer, id_: int | None, edit_text: str = ""):
+ if not layer.isEditable():
+ layer.startEditing()
+ layer.beginEditCommand(edit_text)
+
+ if id_ is None:
+ layer.addFeature(feature)
+ else:
+ layer.updateFeature(feature)
+
+ layer.endEditCommand()
+ layer.commitChanges(stopEditing=False)
+
+
def save_plan(plan_data: Plan) -> QgsFeature:
plan_layer = PlanLayer.get_from_project()
in_edit_mode = plan_layer.isEditable()
@@ -188,6 +215,7 @@ def save_plan(plan_data: Plan) -> QgsFeature:
edit_message = "Kaavan lisäys" if plan_data.id_ is None else "Kaavan muokkaus"
plan_layer.beginEditCommand(edit_message)
+ # plan_data.organisation_id = "99e20d66-9730-4110-815f-5947d3f8abd3"
plan_feature = PlanLayer.feature_from_model(plan_data)
if plan_data.id_ is None:
@@ -202,16 +230,85 @@ def save_plan(plan_data: Plan) -> QgsFeature:
for regulation_group in plan_data.general_regulations:
plan_id = plan_feature["id"]
regulation_group_feature = save_regulation_group(regulation_group, plan_id)
- save_regulation_grop_assosiation(plan_id, regulation_group_feature["id"])
+ save_regulation_group_association(regulation_group_feature["id"], PlanLayer.name, plan_id)
return plan_feature
-def save_regulation_group(regulation_group: RegulationGroup, plan_id: str) -> QgsFeature:
- feature = RegulationGroupLayer.feature_from_model(regulation_group)
- feature["plan_id"] = plan_id
+def save_plan_feature(plan_feature: PlanFeature, plan_id: str | None = None) -> QgsFeature:
+ layer_name_to_class_map: dict[str, type[PlanFeatureLayer]] = {
+ LandUsePointLayer.name: LandUsePointLayer,
+ OtherAreaLayer.name: OtherAreaLayer,
+ OtherPointLayer.name: OtherPointLayer,
+ LandUseAreaLayer.name: LandUseAreaLayer,
+ LineLayer.name: LineLayer,
+ }
+
+ if not plan_feature.layer_name:
+ msg = "Cannot save plan feature without a target layer"
+ raise ValueError(msg)
+ layer_class = layer_name_to_class_map.get(plan_feature.layer_name)
+ if not layer_class:
+ msg = f"Could not find plan feature layer class for layer name {plan_feature.layer_name}"
+ raise ValueError(msg)
+
+ feature = layer_class.feature_from_model(plan_feature, plan_id)
+ layer = layer_class.get_from_project()
+
+ _save_feature(
+ feature=feature,
+ layer=layer,
+ id_=plan_feature.id_,
+ edit_text="Kaavakohteen lisäys" if plan_feature.id_ is None else "Kaavakohteen muokkaus",
+ )
+
+ # Handle regulation groups
+ if plan_feature.regulation_groups:
+ for group in plan_feature.regulation_groups:
+ regulation_group_feature = save_regulation_group(group)
+ save_regulation_group_association(regulation_group_feature["id"], plan_feature.layer_name, feature["id"])
+
+ return feature
+
+
+def save_regulation_group(regulation_group: RegulationGroup, plan_id: str | None = None) -> QgsFeature:
+ feature = RegulationGroupLayer.feature_from_model(regulation_group, plan_id)
+ layer = RegulationGroupLayer.get_from_project()
+
+ _save_feature(
+ feature=feature,
+ layer=layer,
+ id_=regulation_group.id_,
+ edit_text="Kaavamääräysryhmän lisäys" if regulation_group.id_ is None else "Kaavamääräysryhmän muokkaus",
+ )
+
+ # Handle regulations
+ if regulation_group.regulations:
+ for regulation in regulation_group.regulations:
+ regulation.regulation_group_id_ = feature["id"] # Updating regulation group ID
+ save_regulation(regulation)
+
return feature
-def save_regulation_grop_assosiation(plan_id: str, regulation_group_id: str):
- pass
+def save_regulation_group_association(regulation_group_id: str, layer_name: str, feature_id: str) -> QgsFeature:
+ feature = RegulationGroupAssociationLayer.feature_from(regulation_group_id, layer_name, feature_id)
+ layer = RegulationGroupAssociationLayer.get_from_project()
+
+ _save_feature(feature=feature, layer=layer, id_=None, edit_text="Kaavamääräysryhmän assosiaation lisäys")
+
+ return feature
+
+
+def save_regulation(regulation: Regulation) -> QgsFeature:
+ feature = PlanRegulationLayer.feature_from_model(regulation)
+ layer = PlanRegulationLayer.get_from_project()
+
+ _save_feature(
+ feature=feature,
+ layer=layer,
+ id_=regulation.id_,
+ edit_text="Kaavamääräyksen lisäys" if regulation.id_ is None else "Kaavamääräyksen muokkaus",
+ )
+
+ return feature
diff --git a/arho_feature_template/core/plan_regulation_config.py b/arho_feature_template/core/plan_regulation_config.py
deleted file mode 100644
index 180c50a..0000000
--- a/arho_feature_template/core/plan_regulation_config.py
+++ /dev/null
@@ -1,222 +0,0 @@
-from __future__ import annotations
-
-import logging
-import os
-from dataclasses import dataclass, field
-from enum import Enum
-from pathlib import Path
-from typing import TYPE_CHECKING, cast
-
-import yaml
-from qgis.utils import iface
-
-from arho_feature_template.qgis_plugin_tools.tools.resources import resources_path
-from arho_feature_template.utils.misc_utils import get_layer_by_name
-
-if TYPE_CHECKING:
- from numbers import Number
- from typing import Literal
-
- from qgis.core import QgsFeature
- from qgis.gui import QgisInterface
-
- iface: QgisInterface = cast("QgisInterface", iface) # type: ignore[no-redef]
-
-
-logger = logging.getLogger(__name__)
-
-DEFAULT_PLAN_REGULATIONS_CONFIG_PATH = Path(os.path.join(resources_path(), "kaavamaaraykset.yaml"))
-
-
-class ConfigSyntaxError(Exception):
- def __init__(self, message: str):
- super().__init__(f"Invalid config syntax: {message}")
-
-
-class UninitializedError(Exception):
- def __init__(self):
- super().__init__("PlanRegulationsSet is not initialized. Call 'initialize' first")
-
-
-class ValueType(Enum):
- DECIMAL = "desimaali"
- POSITIVE_DECIMAL = "positiivinen desimaali"
- POSITIVE_INTEGER = "positiivinen kokonaisluku"
- POSITIVE_INTEGER_RANGE = "positiivinen kokonaisluku arvoväli"
- VERSIONED_TEXT = "kieliversioitu teksti"
-
-
-# class Unit(Enum):
-# SQUARE_METERS = "k-m2"
-# CUBIC_METERS = "m3"
-# EFFICIENCY_RATIO = "k-m2/m2"
-# PERCENTAGE = "prosentti"
-# AREA_RATIO = "m2/k-m2"
-# DEGREES = "°"
-# DECIBEL = "dB"
-
-
-def get_name_mapping_for_plan_regulations(layer_name: str) -> dict[str, dict[str, str]] | None:
- layer = get_layer_by_name(layer_name)
- if not layer:
- return None
- return {feature["value"]: feature["name"] for feature in layer.getFeatures()}
-
-
-@dataclass
-class PlanRegulationsSet:
- """Describes the set of plan regulations."""
-
- version: str
- regulations: list[PlanRegulationConfig]
- regulations_dict: dict[str, PlanRegulationConfig]
-
- _instance: PlanRegulationsSet | None = None
-
- @classmethod
- def get_instance(cls) -> PlanRegulationsSet:
- """Get the singleton instance, if initialized."""
- if cls._instance is None:
- return cls.initialize()
- return cls._instance
-
- @classmethod
- def get_regulations(cls) -> list[PlanRegulationConfig]:
- """Get the list of top-level regulation configs, if instance is initialized."""
- return cls.get_instance().regulations
-
- @classmethod
- def get_regulations_dict(cls) -> dict[str, PlanRegulationConfig]:
- """Get all regulations in a dictionary where keys are regulations codes and values PlanRegulationConfigs."""
- return cls.get_instance().regulations_dict
-
- @classmethod
- def get_regulation_by_code(cls, regulation_code: str) -> PlanRegulationConfig | None:
- """Get a regulation by it's regulation code (if exists)."""
- return cls.get_instance().regulations_dict.get(regulation_code)
-
- @classmethod
- def initialize(
- cls,
- config_path: Path = DEFAULT_PLAN_REGULATIONS_CONFIG_PATH,
- type_of_plan_regulations_layer_name="Kaavamääräyslaji",
- language: Literal["fin", "eng", "swe"] = "fin",
- ) -> PlanRegulationsSet:
- # Initialize PlanRegulationsSet and PlanRegulationConfigs from QGIS layer and config file
-
- # 1. Read config file into a dict
- with config_path.open(encoding="utf-8") as f:
- config_data = yaml.safe_load(f)
-
- # 2. Read code layer
- layer = get_layer_by_name(type_of_plan_regulations_layer_name)
- if layer is None:
- msg = f"Could not find layer {type_of_plan_regulations_layer_name}!"
- raise KeyError(msg)
-
- # 3. Initialize regulation configs from layer. Storing them by their ID is handy for adding childs later
- id_to_regulation_map: dict[str, PlanRegulationConfig] = {
- feature["id"]: PlanRegulationConfig.from_feature(feature, language) for feature in layer.getFeatures()
- }
-
- # 4. Add information from config file (value, unit, category only) and link child regulations
- try:
- regulation_data: dict = {data["regulation_code"]: data for data in config_data["plan_regulations"]}
- top_level_regulations: list[PlanRegulationConfig] = []
- for regulation_config in id_to_regulation_map.values():
- # Add possible information from config data file
- data = regulation_data.get(regulation_config.regulation_code)
- if data:
- regulation_config.category_only = data.get("category_only", False)
- regulation_config.value_type = ValueType(data["value_type"]) if "value_type" in data else None
- regulation_config.unit = data["unit"] if "unit" in data else None
-
- # Top-level, add to list
- if not regulation_config.parent_id:
- top_level_regulations.append(regulation_config)
- else:
- # Add as child of another regulation
- id_to_regulation_map[regulation_config.parent_id].child_regulations.append(regulation_config)
- except KeyError as e:
- raise ConfigSyntaxError(str(e)) from e
-
- # 5. Create dictionary, useful when creating PlanRegulationDefinitions at least
- regulations_dict: dict[str, PlanRegulationConfig] = {}
- for reg in top_level_regulations:
- reg.add_to_dictionary(regulations_dict)
-
- # 5. Create instance
- cls._instance = cls(
- version=config_data["version"], regulations=top_level_regulations, regulations_dict=regulations_dict
- )
- logger.info("PlanRegulationsSet initialized successfully.")
- return cls._instance
-
-
-@dataclass
-class PlanRegulationConfig:
- """
- Describes the configuration of a plan regulation.
-
- Combination of information read from code layer in QGIS / DB and other information
- from a configuration file.
- """
-
- id: str
- regulation_code: str
- name: str
- description: str
- status: str
- level: int
- parent_id: str | None
- child_regulations: list[PlanRegulationConfig] = field(default_factory=list)
-
- category_only: bool = False
- value_type: ValueType | None = None
- unit: str | None = None
-
- @classmethod
- def from_feature(cls, feature: QgsFeature, language: str = "fin") -> PlanRegulationConfig:
- """
- Initialize PlanRegulationConfig from QgsFeature.
-
- Child regulations, value type ,category only and unit need to be set separately.
- """
- return cls(
- id=feature["id"],
- regulation_code=feature["value"],
- name=feature["name"][language],
- description=feature["description"][language],
- status=feature["status"],
- level=feature["level"],
- parent_id=feature["parent_id"],
- )
-
- def add_to_dictionary(self, dictionary: dict[str, PlanRegulationConfig]):
- """Add child regulations to dictionary too."""
- dictionary[self.regulation_code] = self
- for regulation in self.child_regulations:
- regulation.add_to_dictionary(dictionary)
-
-
-@dataclass
-class PlanRegulationDefinition:
- """Associates a PlanRegulationConfig with an optional default value and additional data."""
-
- regulation_config: PlanRegulationConfig
- default_value: str | Number | list[int] | None
- additional_information: list[
- dict[str, str | Number | None]
- ] # NOTE: Correct typing for additional information values?
- regulation_number: int | None
- attached_files: list[Path]
-
- @classmethod
- def from_dict(cls, data: dict) -> PlanRegulationDefinition:
- return cls(
- regulation_config=data["config"],
- default_value=data.get("default_value"),
- additional_information=data.get("additional_information", []),
- regulation_number=data.get("regulation_number"),
- attached_files=data.get("attached_files", []),
- )
diff --git a/arho_feature_template/core/plan_regulation_group_config.py b/arho_feature_template/core/plan_regulation_group_config.py
deleted file mode 100644
index 0d455eb..0000000
--- a/arho_feature_template/core/plan_regulation_group_config.py
+++ /dev/null
@@ -1,118 +0,0 @@
-from __future__ import annotations
-
-import logging
-from dataclasses import dataclass
-from typing import TYPE_CHECKING, cast
-
-import yaml
-from qgis.utils import iface
-
-from arho_feature_template.core.plan_regulation_config import PlanRegulationDefinition, PlanRegulationsSet
-
-if TYPE_CHECKING:
- from pathlib import Path
-
- from qgis.gui import QgisInterface
-
- iface: QgisInterface = cast("QgisInterface", iface) # type: ignore[no-redef]
-
-logger = logging.getLogger(__name__)
-
-
-class ConfigSyntaxError(Exception):
- def __init__(self, message: str):
- super().__init__(f"Invalid config syntax: {message}")
-
-
-@dataclass
-class PlanRegulationGroupLibrary:
- """Describes the configuration of a plan regulation group library"""
-
- meta: PlanRegulationGroupLibraryMeta
- plan_regulation_group_categories: list[PlanRegulationGroupCategory]
-
- @classmethod
- def from_dict(cls, data: dict) -> PlanRegulationGroupLibrary:
- try:
- return cls(
- meta=PlanRegulationGroupLibraryMeta.from_dict(data["meta"]),
- plan_regulation_group_categories=[
- PlanRegulationGroupCategory.from_dict(category) for category in data["categories"]
- ],
- )
- except KeyError as e:
- raise ConfigSyntaxError(str(e)) from e
-
- @classmethod
- def new_from_file(cls, fp: Path) -> PlanRegulationGroupLibrary:
- with fp.open(encoding="utf-8") as f:
- data = yaml.safe_load(f)
- return PlanRegulationGroupLibrary.from_dict(data)
-
-
-@dataclass
-class PlanRegulationGroupLibraryMeta:
- """Describes the metadata of a plan regulation group library"""
-
- name: str
- version: int | None
- group: str | None
- sub_group: str | None
- description: str | None
-
- @classmethod
- def from_dict(cls, data: dict) -> PlanRegulationGroupLibraryMeta:
- return cls(
- name=data["name"],
- version=data.get("version"),
- group=data.get("group"),
- sub_group=data.get("sub_group"),
- description=data.get("description"),
- )
-
-
-@dataclass
-class PlanRegulationGroupCategory:
- category_code: str
- name: str | None
- plan_regulation_groups: list[PlanRegulationGroupDefinition]
-
- @classmethod
- def from_dict(cls, data: dict) -> PlanRegulationGroupCategory:
- return cls(
- category_code=data["category_code"],
- name=data.get("name"),
- plan_regulation_groups=[
- PlanRegulationGroupDefinition.from_dict(group) for group in data["plan_regulation_groups"]
- ],
- )
-
-
-@dataclass
-class PlanRegulationGroupDefinition:
- """Describes a plan regulation group"""
-
- name: str
- geometry: str
- color_code: str | None
- letter_code: str | None
- plan_regulations: list[PlanRegulationDefinition]
-
- @classmethod
- def from_dict(cls, data: dict) -> PlanRegulationGroupDefinition:
- regulations = []
- for reg_data in data["plan_regulations"]:
- reg_code = reg_data["regulation_code"]
- config = PlanRegulationsSet.get_regulation_by_code(reg_code)
- if config:
- reg_data["config"] = config
- regulations.append(PlanRegulationDefinition.from_dict(reg_data))
- else:
- iface.messageBar().pushWarning("", f"Could not find config for {reg_code} plan regulation!")
- return cls(
- name=data["name"],
- geometry=data["geometry"],
- color_code=data.get("color_code"),
- letter_code=data.get("letter_code"),
- plan_regulations=regulations,
- )
diff --git a/arho_feature_template/exceptions.py b/arho_feature_template/exceptions.py
index fb4eb43..7093c57 100644
--- a/arho_feature_template/exceptions.py
+++ b/arho_feature_template/exceptions.py
@@ -19,3 +19,13 @@ def __init__(self):
class LayerNotVectorTypeError(Exception):
def __init__(self, layer_name: str):
super().__init__(f"Layer {layer_name} is not a vector layer")
+
+
+class ConfigSyntaxError(Exception):
+ def __init__(self, message: str):
+ super().__init__(f"Invalid config syntax: {message}")
+
+
+class UninitializedError(Exception):
+ def __init__(self):
+ super().__init__("RegulationLibrary is not initialized. Call 'initialize' first")
diff --git a/arho_feature_template/gui/new_plan_regulation_group_form.py b/arho_feature_template/gui/new_plan_regulation_group_form.py
index 198e434..995eae6 100644
--- a/arho_feature_template/gui/new_plan_regulation_group_form.py
+++ b/arho_feature_template/gui/new_plan_regulation_group_form.py
@@ -7,8 +7,8 @@
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtWidgets import QDialog, QTextBrowser, QTreeWidget, QTreeWidgetItem
-from arho_feature_template.core.plan_regulation_config import PlanRegulationConfig, PlanRegulationsSet
-from arho_feature_template.gui.plan_regulation_widget import PlanRegulationWidget
+from arho_feature_template.core.models import Regulation, RegulationConfig, RegulationLibrary
+from arho_feature_template.gui.plan_regulation_widget import RegulationWidget
if TYPE_CHECKING:
from qgis.PyQt.QtWidgets import QBoxLayout, QWidget
@@ -35,7 +35,7 @@ def __init__(self):
self.plan_regulations_view.itemDoubleClicked.connect(self.add_selected_plan_regulation)
self.plan_regulations_view.itemClicked.connect(self.update_selected_plan_regulation)
- def _initalize_regulation_from_config(self, config: PlanRegulationConfig, parent: QTreeWidgetItem | None = None):
+ def _initalize_regulation_from_config(self, config: RegulationConfig, parent: QTreeWidgetItem | None = None):
tree_item = QTreeWidgetItem(parent)
tree_item.setText(0, config.name)
tree_item.setData(0, Qt.UserRole, config)
@@ -49,25 +49,26 @@ def _initalize_regulation_from_config(self, config: PlanRegulationConfig, parent
self._initalize_regulation_from_config(child_config, tree_item)
def initialize_plan_regulations(self):
- for config in PlanRegulationsSet.get_regulations():
+ for config in RegulationLibrary.get_regulations():
self._initalize_regulation_from_config(config)
def update_selected_plan_regulation(self, item: QTreeWidgetItem, column: int):
- config: PlanRegulationConfig = item.data(column, Qt.UserRole) # Retrieve the associated config
+ config: RegulationConfig = item.data(column, Qt.UserRole) # Retrieve the associated config
self.plan_regulation_info.setText(config.description)
def add_selected_plan_regulation(self, item: QTreeWidgetItem, column: int):
- config: PlanRegulationConfig = item.data(column, Qt.UserRole) # Retrieve the associated config
+ config: RegulationConfig = item.data(column, Qt.UserRole) # Retrieve the associated config
if config.category_only:
return
self.add_plan_regulation(config)
- def add_plan_regulation(self, config: PlanRegulationConfig):
- widget = PlanRegulationWidget.from_config(config=config, parent=self.plan_regulations_scroll_area_contents)
+ def add_plan_regulation(self, config: RegulationConfig):
+ regulation = Regulation(config=config)
+ widget = RegulationWidget(regulation, parent=self.plan_regulations_scroll_area_contents)
widget.delete_signal.connect(self.delete_plan_regulation)
index = self.plan_regulations_layout.count() - 1
self.plan_regulations_layout.insertWidget(index, widget)
- def delete_plan_regulation(self, plan_regulation_widget: PlanRegulationWidget):
+ def delete_plan_regulation(self, plan_regulation_widget: RegulationWidget):
self.plan_regulations_layout.removeWidget(plan_regulation_widget)
plan_regulation_widget.deleteLater()
diff --git a/arho_feature_template/gui/plan_regulation_group_widget.py b/arho_feature_template/gui/plan_regulation_group_widget.py
index 89f51a0..5ebdeec 100644
--- a/arho_feature_template/gui/plan_regulation_group_widget.py
+++ b/arho_feature_template/gui/plan_regulation_group_widget.py
@@ -8,51 +8,79 @@
from qgis.PyQt.QtCore import pyqtSignal
from qgis.PyQt.QtWidgets import QWidget
-from arho_feature_template.gui.plan_regulation_widget import PlanRegulationWidget
+from arho_feature_template.core.models import Regulation, RegulationGroup
+from arho_feature_template.gui.plan_regulation_widget import RegulationWidget
+from arho_feature_template.project.layers.code_layers import PlanRegulationGroupTypeLayer
if TYPE_CHECKING:
- from qgis.PyQt.QtWidgets import QFrame, QLineEdit, QPushButton
+ from qgis.PyQt.QtWidgets import QFormLayout, QFrame, QLabel, QLineEdit, QPushButton
- from arho_feature_template.core.plan_regulation_config import PlanRegulationDefinition
- from arho_feature_template.core.plan_regulation_group_config import PlanRegulationGroupDefinition
+ from arho_feature_template.gui.code_combobox import CodeComboBox
ui_path = resources.files(__package__) / "plan_regulation_group_widget.ui"
FormClass, _ = uic.loadUiType(ui_path)
-class PlanRegulationGroupWidget(QWidget, FormClass): # type: ignore
+class RegulationGroupWidget(QWidget, FormClass): # type: ignore
"""A widget representation of a plan regulation group."""
delete_signal = pyqtSignal(QWidget)
- def __init__(self, group_definition: PlanRegulationGroupDefinition):
+ def __init__(self, regulation_group_data: RegulationGroup):
super().__init__()
self.setupUi(self)
# TYPES
self.frame: QFrame
- self.heading: QLineEdit
+ self.name: QLineEdit
+ self.short_name: QLineEdit
self.del_btn: QPushButton
+ self.type_of_regulation_group_label: QLabel
+ self.type_of_regulation_group: CodeComboBox
+ self.regulation_group_details_layout: QFormLayout
+ # NOTE: Maybe user input is not needed and wanted for type of plan regulation group and it would be defined
+ # by the plan feature directly (and hidden from user)
# INIT
- self.group_definition = group_definition
- self.layer = "plan_regulation_group"
-
- self.heading.setText(self.group_definition.name)
- self.init_buttons()
- for plan_regulation_definition in self.group_definition.plan_regulations:
- _ = self.add_plan_regulation_widget(plan_regulation_definition)
-
- def init_buttons(self):
+ self.regulation_group_data = regulation_group_data
+ self.regulation_widgets: list[RegulationWidget] = [
+ self.add_regulation_widget(regulation) for regulation in self.regulation_group_data.regulations
+ ]
+
+ # If regulation group type code is defined, delete selection for user
+ if regulation_group_data.type_code_id:
+ self.regulation_group_details_layout.removeWidget(self.type_of_regulation_group_label)
+ self.regulation_group_details_layout.removeWidget(self.type_of_regulation_group)
+ self.type_of_regulation_group_label.deleteLater()
+ self.type_of_regulation_group.deleteLater()
+ else:
+ self.type_of_regulation_group.populate_from_code_layer(PlanRegulationGroupTypeLayer)
+ self.type_of_regulation_group.removeItem(0) # Remove NULL from combobox as underground data is required
+
+ self.name.setText(self.regulation_group_data.name if self.regulation_group_data.name else "")
+ self.short_name.setText(self.regulation_group_data.short_name if self.regulation_group_data.short_name else "")
self.del_btn.setIcon(QgsApplication.getThemeIcon("mActionDeleteSelected.svg"))
self.del_btn.clicked.connect(lambda: self.delete_signal.emit(self))
- def add_plan_regulation_widget(self, definition: PlanRegulationDefinition) -> PlanRegulationWidget:
- widget = PlanRegulationWidget.from_definition(definition=definition, parent=self.frame)
- widget.delete_signal.connect(self.delete_plan_regulation_widget)
+ def add_regulation_widget(self, regulation: Regulation) -> RegulationWidget:
+ widget = RegulationWidget(regulation=regulation, parent=self.frame)
+ widget.delete_signal.connect(self.delete_regulation_widget)
self.frame.layout().addWidget(widget)
return widget
- def delete_plan_regulation_widget(self, plan_regulation_widget: PlanRegulationWidget):
- self.frame.layout().removeWidget(plan_regulation_widget)
- plan_regulation_widget.deleteLater()
+ def delete_regulation_widget(self, regulation_widget: RegulationWidget):
+ self.frame.layout().removeWidget(regulation_widget)
+ self.regulation_widgets.remove(regulation_widget)
+ regulation_widget.deleteLater()
+
+ def into_model(self) -> RegulationGroup:
+ return RegulationGroup(
+ type_code_id=self.regulation_group_data.type_code_id
+ if self.regulation_group_data.type_code_id
+ else self.type_of_regulation_group.value(),
+ name=self.name.text(),
+ short_name=self.short_name.text(),
+ color_code=self.regulation_group_data.color_code,
+ regulations=[widget.into_model() for widget in self.regulation_widgets],
+ id_=self.regulation_group_data.id_,
+ )
diff --git a/arho_feature_template/gui/plan_regulation_group_widget.ui b/arho_feature_template/gui/plan_regulation_group_widget.ui
index d6bd911..11f4ee8 100644
--- a/arho_feature_template/gui/plan_regulation_group_widget.ui
+++ b/arho_feature_template/gui/plan_regulation_group_widget.ui
@@ -6,8 +6,8 @@
0
0
- 514
- 65
+ 420
+ 160
@@ -38,20 +38,74 @@
-
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 30
+ 16777215
+
+
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
- 75
- true
+ 50
+ false
- Otsikko
+ Nimi
- -
-
+
-
+
0
@@ -63,25 +117,44 @@
- -
-
+
-
+
-
+
0
0
+
+
+ 0
+ 0
+
+
- 30
+ 16777215
16777215
-
+ Tyyppi
+ -
+
+
+ -
+
+
+ Lyhyt nimi
+
+
+
+ -
+
+
@@ -89,6 +162,13 @@
+
+
+ CodeComboBox
+ QComboBox
+ arho_feature_template.gui.code_combobox
+
+
diff --git a/arho_feature_template/gui/plan_regulation_input_widgets.py b/arho_feature_template/gui/plan_regulation_input_widgets.py
new file mode 100644
index 0000000..1d65659
--- /dev/null
+++ b/arho_feature_template/gui/plan_regulation_input_widgets.py
@@ -0,0 +1,115 @@
+from __future__ import annotations
+
+from qgis.gui import QgsDoubleSpinBox, QgsSpinBox
+from qgis.PyQt.QtWidgets import QHBoxLayout, QLineEdit, QSizePolicy, QTextEdit, QWidget
+
+
+def initialize_numeric_input_widget(
+ widget: QgsSpinBox | QgsDoubleSpinBox,
+ default_value: float | None,
+ unit: str | None,
+ positive: bool, # noqa: FBT001
+):
+ if unit:
+ widget.setSuffix(f" {unit}")
+
+ if positive:
+ widget.setMinimum(0)
+ else:
+ widget.setMinimum(-99999)
+
+ widget.setMaximum(99999)
+
+ if default_value:
+ widget.setValue(default_value)
+
+ widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+
+
+def initialize_text_input_widget(
+ widget: QTextEdit | QLineEdit,
+ default_value: str | None,
+ editable: bool, # noqa: FBT001
+):
+ if default_value:
+ widget.setText(str(default_value))
+
+ if not editable:
+ widget.setReadOnly(True)
+
+
+class DecimalInputWidget(QgsDoubleSpinBox):
+ def __init__(
+ self,
+ default_value: float | None = None,
+ unit: str | None = None,
+ positive: bool = False, # noqa: FBT001, FBT002
+ ):
+ super().__init__()
+ self.unit = unit
+ initialize_numeric_input_widget(self, default_value, unit, positive)
+
+ def get_value(self) -> float:
+ return self.value()
+
+
+class IntegerInputWidget(QgsSpinBox):
+ def __init__(
+ self,
+ default_value: int | None = None,
+ unit: str | None = None,
+ positive: bool = False, # noqa: FBT001, FBT002
+ ):
+ super().__init__()
+ self.unit = unit
+ initialize_numeric_input_widget(self, default_value, unit, positive)
+
+ def get_value(self) -> int:
+ return self.value()
+
+
+class IntegerRangeInputWidget(QWidget):
+ def __init__(
+ self,
+ default_value: tuple[int, int] | list[int] | None = None,
+ unit: str | None = None,
+ positive: bool = False, # noqa: FBT001, FBT002
+ ):
+ super().__init__()
+ if isinstance(default_value, list):
+ default_value = (default_value[0], default_value[1])
+ self.min_widget = IntegerInputWidget(default_value[0] if default_value else None, unit, positive)
+ self.max_widget = IntegerInputWidget(default_value[1] if default_value else None, unit, positive)
+ layout = QHBoxLayout()
+ layout.addWidget(self.min_widget)
+ layout.addWidget(self.max_widget)
+ self.setLayout(layout)
+
+ def get_value(self) -> tuple[int, int]:
+ return (self.min_widget.get_value(), self.max_widget.get_value())
+
+
+class SinglelineTextInputWidget(QLineEdit):
+ def __init__(
+ self,
+ default_value: str | None = None,
+ editable: bool = False, # noqa: FBT001, FBT002
+ ):
+ super().__init__()
+ initialize_text_input_widget(self, default_value, editable)
+
+ def get_value(self) -> str | None:
+ return self.text() if self.text() else None
+
+
+class MultilineTextInputWidget(QTextEdit):
+ def __init__(
+ self,
+ default_value: str | None = None,
+ editable: bool = True, # noqa: FBT001, FBT002
+ ):
+ super().__init__()
+ initialize_text_input_widget(self, default_value, editable)
+
+ def get_value(self) -> str | None:
+ return self.toPlainText() if self.toPlainText() else None
diff --git a/arho_feature_template/gui/plan_regulation_widget.py b/arho_feature_template/gui/plan_regulation_widget.py
index 0c11aa5..9320300 100644
--- a/arho_feature_template/gui/plan_regulation_widget.py
+++ b/arho_feature_template/gui/plan_regulation_widget.py
@@ -1,11 +1,10 @@
from __future__ import annotations
from importlib import resources
-from numbers import Number
-from typing import TYPE_CHECKING, cast
+from typing import TYPE_CHECKING
from qgis.core import QgsApplication
-from qgis.gui import QgsDoubleSpinBox, QgsFileWidget, QgsSpinBox
+from qgis.gui import QgsFileWidget
from qgis.PyQt import uic
from qgis.PyQt.QtCore import Qt, pyqtSignal
from qgis.PyQt.QtWidgets import (
@@ -13,151 +12,128 @@
QLabel,
QLineEdit,
QMenu,
- QSizePolicy,
- QTextEdit,
QToolButton,
QWidget,
)
-from arho_feature_template.core.plan_regulation_config import PlanRegulationConfig, ValueType
-from arho_feature_template.utils.misc_utils import get_additional_information_name, get_layer_by_name
+from arho_feature_template.core.models import Regulation, ValueType
+from arho_feature_template.gui.plan_regulation_input_widgets import (
+ DecimalInputWidget,
+ IntegerInputWidget,
+ IntegerRangeInputWidget,
+ MultilineTextInputWidget,
+ SinglelineTextInputWidget,
+)
+from arho_feature_template.utils.misc_utils import get_additional_information_name, get_layer_by_name, iface
if TYPE_CHECKING:
from qgis.PyQt.QtWidgets import QPushButton
- from arho_feature_template.core.plan_regulation_group_config import PlanRegulationDefinition
-
ui_path = resources.files(__package__) / "plan_regulation_widget.ui"
FormClass, _ = uic.loadUiType(ui_path)
-
-# Related layer and field names to save information later on
-LAYER_NAME = "Kaavamääräys"
-TYPE_OF_PLAN_REGULATION_KIND_FIELD = "type_of_plan_regulation_kind"
-NUMERIC_VALUE_FIELD = "numeric_value"
-TYPE_OF_VERBAL_PLAN_REGULATION_FIELD = "type_of_verbal_plan_regulation"
-UNIT_FIELD = "unit"
-TEXT_VALUE_FIELD = "text_value"
-REGULATION_TYPE_ADDITIONAL_INFORMATION_ID = "regulation_type_additional_information_id"
-
# TO BE REPLACED
-ADDITIONAL_INFORMATION_TYPES_WITH_INPUT = ["kayttotarkoituskohdistus"]
+LANGUAGE = "fin"
-class PlanRegulationWidget(QWidget, FormClass): # type: ignore
+class RegulationWidget(QWidget, FormClass): # type: ignore
"""A widget representation of a plan regulation."""
- language = "fin"
-
delete_signal = pyqtSignal(QWidget)
- def __init__(self, config: PlanRegulationConfig, parent=None):
+ def __init__(self, regulation: Regulation, parent=None):
super().__init__(parent)
self.setupUi(self)
# TYPES
self.plan_regulation_name: QLineEdit
self.form_layout: QFormLayout
-
self.add_additional_information_btn: QPushButton
self.add_field_btn: QPushButton
self.del_btn: QPushButton
self.expand_hide_btn: QToolButton
-
self.code_label: QLabel
self.code: QLineEdit
# INIT
- self.config = config
- self.regulation_number_added = False
- self.expanded = True
+ self.config = regulation.config
+ self.regulation = regulation
+
+ # List of widgets for hiding / showing
self.widgets: list[tuple[QLabel, QWidget]] = []
- self.plan_regulation_name.setText(config.name)
+
+ # For accessing correct widgets when data is sent
+ self.value_widget: QWidget | None = None
+ self.regulation_number_widget: IntegerInputWidget | None = None
+ self.additional_information_widgets: dict[str, QWidget | None] = {} # Key = information type, value = widget
+ self.file_widgets: list[QgsFileWidget] = []
+ self.theme_widget: SinglelineTextInputWidget | None = None
+ self.topic_tag_widget: SinglelineTextInputWidget | None = None
+
+ self.expanded = True
+ self.plan_regulation_name.setText(self.config.name)
self.plan_regulation_name.setReadOnly(True)
self.del_btn.setIcon(QgsApplication.getThemeIcon("mActionDeleteSelected.svg"))
self.del_btn.clicked.connect(lambda: self.delete_signal.emit(self))
self.expand_hide_btn.clicked.connect(self._on_expand_hide_btn_clicked)
- self.init_additional_information_btn()
- self.init_other_information_btn()
-
- @classmethod
- def from_config(cls, config: PlanRegulationConfig, parent=None) -> PlanRegulationWidget:
- instance = cls(config, parent)
- instance.init_fields()
- return instance
-
- @classmethod
- def from_definition(cls, definition: PlanRegulationDefinition, parent=None) -> PlanRegulationWidget:
- instance = cls(definition.regulation_config, parent)
- instance.init_fields_from_definition(definition)
- return instance
-
- def init_fields_from_definition(self, definition: PlanRegulationDefinition):
+ self._init_additional_information_btn()
+ self._init_other_information_btn()
+ self._init_widgets()
+
+ def _init_widgets(self):
# Value input
value_type = self.config.value_type
if value_type:
- self._add_value_input(value_type, self.config.unit, definition.default_value)
+ self._add_value_input(value_type, self.config.unit, self.regulation.value)
# Additional information
- for info in definition.additional_information:
- info_type: str = cast("str", info["type"])
- self.add_additional_info(info_type, info.get("value"))
-
- # TODO: Other saved information from PlanRegulationDefinition
-
- def init_fields(self):
- value_type = self.config.value_type
- if value_type:
- self._add_value_input(value_type, self.config.unit)
-
- @staticmethod
- def _check_number_or_none(value: str | Number | None, error_msg: str):
- if not isinstance(value, Number) and value is not None:
- raise ValueError(error_msg)
+ if self.regulation.additional_information:
+ for info_type, info_value in self.regulation.additional_information.items():
+ self._add_additional_info(info_type, info_value)
def _add_value_input(
- self, value_type: ValueType, unit: str | None, default_value: str | Number | list[int] | None = None
+ self, value_type: ValueType, unit: str | None, default_value: str | float | list[int] | None = None
):
base_error_msg = f"Invalid type for default value {type(default_value)}."
if value_type in [ValueType.DECIMAL, ValueType.POSITIVE_DECIMAL]:
- if not isinstance(default_value, Number) and default_value is not None:
+ if not isinstance(default_value, float) and default_value is not None:
raise ValueError(base_error_msg)
- self.add_decimal_input(value_type, unit, default_value)
+ self._add_decimal_input(value_type, unit, default_value)
elif value_type == ValueType.POSITIVE_INTEGER:
if not isinstance(default_value, int) and default_value is not None:
raise ValueError(base_error_msg)
- self.add_positive_integer_input(unit, default_value)
+ self._add_integer_input(value_type, unit, default_value)
elif value_type == ValueType.POSITIVE_INTEGER_RANGE:
if not isinstance(default_value, list) and default_value is not None:
raise ValueError(base_error_msg)
if isinstance(default_value, list) and len(default_value) != 2: # noqa: PLR2004
error_msg = f"Invalid number of values in default value {type(default_value)}."
raise ValueError(error_msg)
- self.add_positive_integer_range_input(unit, default_value)
+ self._add_integer_range_input(value_type, unit, default_value)
elif value_type == ValueType.VERSIONED_TEXT:
if not isinstance(default_value, str) and default_value is not None:
raise ValueError(base_error_msg)
- self.add_versioned_text_input(default_value)
+ self._add_versioned_text_input(default_value)
else:
msg = f"Invalid input value type for plan regulation: {value_type}"
raise ValueError(msg)
- def init_additional_information_btn(self):
+ def _init_additional_information_btn(self):
informations_dict: dict[str, QMenu] = {}
add_later: dict[str, list[str]] = {}
def _add_action(informations_dict: dict[str, QMenu], parent_id: str, info_type: str):
action = informations_dict[parent_id].addAction(info_type)
- action.triggered.connect(lambda _: self.add_additional_info(info_type))
+ action.triggered.connect(lambda _: self._add_additional_info(info_type))
# Iterate code layer and build menus
for feature in get_layer_by_name("Lisätiedonlaji").getFeatures():
if feature["level"] == 1:
- menu = QMenu(feature["name"][self.language], self)
+ menu = QMenu(feature["name"][LANGUAGE], self)
informations_dict[feature["id"]] = menu
else:
parent_id = feature["parent_id"]
- info_type = feature["name"][self.language]
+ info_type = feature["name"][LANGUAGE]
if parent_id in informations_dict:
_add_action(informations_dict, parent_id, info_type)
else:
@@ -175,17 +151,17 @@ def _add_action(informations_dict: dict[str, QMenu], parent_id: str, info_type:
self.add_additional_information_btn.setMenu(additional_information_type_menu)
self.add_additional_information_btn.setIcon(QgsApplication.getThemeIcon("mActionPropertiesWidget.svg"))
- def init_other_information_btn(self):
+ def _init_other_information_btn(self):
add_field_menu = QMenu(self)
- add_field_menu.addAction("Määräysnumero").triggered.connect(self.add_regulation_number)
- add_field_menu.addAction("Liiteasiakirja").triggered.connect(self.add_file)
- add_field_menu.addAction("Aihetunniste").triggered.connect(self.add_topic_tag)
+ add_field_menu.addAction("Määräysnumero").triggered.connect(self._add_regulation_number)
+ add_field_menu.addAction("Liiteasiakirja").triggered.connect(self._add_file)
+ add_field_menu.addAction("Aihetunniste").triggered.connect(self._add_topic_tag)
theme_menu = QMenu("Kaavoitusteema", self)
for feature in get_layer_by_name("Kaavoitusteemat").getFeatures():
- name = feature["name"][self.language]
+ name = feature["name"][LANGUAGE]
action = theme_menu.addAction(name)
- action.triggered.connect(lambda _, name=name: self.add_theme(name))
+ action.triggered.connect(lambda _, name=name: self._add_theme(name))
add_field_menu.addMenu(theme_menu)
self.add_field_btn.setMenu(add_field_menu)
@@ -208,103 +184,83 @@ def _on_expand_hide_btn_clicked(self):
self.expand_hide_btn.setArrowType(Qt.ArrowType.UpArrow)
self.expanded = True
- def _add_widgets_to_form(self, label: QLabel, widget: QWidget):
+ def _add_widgets(self, label: QLabel, widget: QWidget):
self.form_layout.addRow(label, widget)
self.widgets.append((label, widget))
if not self.expanded:
self._on_expand_hide_btn_clicked()
- def add_decimal_input(self, value_type: ValueType, unit: str | None, default_value: Number | None = None):
- value_widget = QgsDoubleSpinBox()
- label = QLabel("Arvo")
- if value_type == ValueType.POSITIVE_DECIMAL:
- value_widget.setMinimum(0.0)
- label.setToolTip("Tyyppi: desimaali (positiivinen)")
- else:
- value_widget.setMinimum(-9999.9)
- label.setToolTip("Tyyppi: desimaali")
- value_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
- if unit:
- value_widget.setSuffix(f" {unit}")
- if default_value:
- value_widget.setValue(default_value)
- self._add_widgets_to_form(label, value_widget)
-
- def add_positive_integer_input(self, unit: str | None, default_value: int | None = None):
- value_widget = QgsSpinBox()
- value_widget.setMinimum(0)
- value_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
- if unit:
- value_widget.setSuffix(f" {unit}")
- label = QLabel("Arvo")
- label.setToolTip("Tyyppi: kokonaisluku (positiivinen)")
- if default_value:
- value_widget.setValue(default_value)
- self._add_widgets_to_form(label, value_widget)
-
- def add_positive_integer_range_input(self, unit: str | None, default_values: list[int] | None = None):
- min_widget = QgsSpinBox()
- min_widget.setMinimum(0)
- min_label = QLabel("Arvo minimi")
- min_label.setToolTip("Tyyppi: kokonaisluku arvoväli (positiivinen)")
-
- max_widget = QgsSpinBox()
- max_widget.setMinimum(0)
- max_label = QLabel("Arvo maksimi")
- max_label.setToolTip("Tyyppi: kokonaisluku arvoväli (positiivinen)")
- if unit:
- min_widget.setSuffix(f" {unit}")
- max_widget.setSuffix(f" {unit}")
- if default_values:
- min_widget.setValue(default_values[0])
- max_widget.setValue(default_values[1])
- self._add_widgets_to_form(min_label, min_widget)
- self._add_widgets_to_form(max_label, max_widget)
-
- def add_versioned_text_input(self, default_value: str | None = None):
- text_widget = QTextEdit()
- label = QLabel("Arvo")
- label.setToolTip("Tyyppi: kieliversioitu teksti")
- if default_value:
- text_widget.setText(default_value)
- self._add_widgets_to_form(label, text_widget)
-
- def add_additional_info(self, info_type: str, default_value: str | Number | None = None):
+ def _add_decimal_input(self, value_type: ValueType, unit: str | None, default_value: float | None = None):
+ positive = value_type == ValueType.POSITIVE_DECIMAL
+ self.value_widget = DecimalInputWidget(default_value, unit, positive)
+ self._add_widgets(QLabel("Arvo"), self.value_widget)
+
+ def _add_integer_input(self, value_type: ValueType, unit: str | None, default_value: int | None = None):
+ positive = value_type == ValueType.POSITIVE_INTEGER
+ self.value_widget = IntegerInputWidget(default_value=default_value, unit=unit, positive=positive)
+ self._add_widgets(QLabel("Arvo"), self.value_widget)
+
+ def _add_integer_range_input(
+ self, value_type: ValueType, unit: str | None, default_value: tuple[int, int] | list[int] | None = None
+ ):
+ # NOTE: There is no ValueType.INTEGER_RANGE currently, so is always positive
+ positive = value_type == ValueType.POSITIVE_INTEGER_RANGE
+ self.value_widget = IntegerRangeInputWidget(default_value, unit, positive)
+ self._add_widgets(QLabel("Arvo"), self.value_widget)
+
+ def _add_versioned_text_input(self, default_value: str | None = None):
+ self.value_widget = MultilineTextInputWidget(default_value=default_value, editable=True)
+ self._add_widgets(QLabel("Arvo"), self.value_widget)
+
+ def _add_additional_info(self, info_type: str, default_value: str | float | None = None):
+ # TODO: Extend and make sure all additional information types are properly handled
+
# NOTE: Now info type is the name / readable version when this is triggered by user
# Might need to refactor this later..
name = get_additional_information_name(info_type)
- info_type_line = QLineEdit(name)
- info_type_line.setReadOnly(True)
- label = QLabel("Lisätiedonlaji")
- self._add_widgets_to_form(label, info_type_line)
+ self._add_widgets(QLabel("Lisätiedonlaji"), SinglelineTextInputWidget(name, False))
+ # NOTE: Does not support multiple instances of same additional information kind,
+ # for example if multiple Käyttötarkoituskohdistus are added, they overwrite each other
+ value_widget = None
if name == "Käyttötarkoituskohdistus":
- info_value_widget = QLineEdit()
- info_value_label = QLabel(name)
- self._add_widgets_to_form(info_value_label, info_value_widget)
-
- if default_value:
- info_value_widget.setText(str(default_value))
-
- def add_regulation_number(self):
- if not self.regulation_number_added:
- number_widget = QgsSpinBox()
- label = QLabel("Määräysnumero")
- self._add_widgets_to_form(label, number_widget)
- self.regulation_number_added = True
-
- def add_file(self):
- file_input = QgsFileWidget()
- label = QLabel("Liiteasiakirja")
- self._add_widgets_to_form(label, file_input)
-
- def add_topic_tag(self):
- text_input = QLineEdit()
- label = QLabel("Aihetunniste")
- self._add_widgets_to_form(label, text_input)
-
- def add_theme(self, theme_name: str):
- theme_type_line = QLineEdit(theme_name)
- theme_type_line.setReadOnly(True)
- label = QLabel("Kaavoitusteema")
- self._add_widgets_to_form(label, theme_type_line)
+ if isinstance(default_value, float):
+ iface.messageBar().pushWarning("Warning: ", f"Unexpected value type for {name}: float")
+ else:
+ value_widget = SinglelineTextInputWidget(default_value, True)
+ self._add_widgets(QLabel(name), value_widget)
+
+ self.additional_information_widgets[info_type] = value_widget
+
+ def _add_regulation_number(self):
+ if not self.regulation_number_widget:
+ self.regulation_number_widget = IntegerInputWidget(None, None, True)
+ self._add_widgets(QLabel("Määräysnumero"), self.regulation_number_widget)
+
+ def _add_file(self):
+ widget = QgsFileWidget()
+ self._add_widgets(QLabel("Liiteasiakirja"), widget)
+ self.file_widgets.append(widget)
+
+ def _add_topic_tag(self):
+ self.topic_tag_widget = SinglelineTextInputWidget(None, True)
+ self._add_widgets(QLabel("Aihetunniste"), self.topic_tag_widget)
+
+ def _add_theme(self, theme_name: str):
+ self.theme_widget = SinglelineTextInputWidget(theme_name, False)
+ self._add_widgets(QLabel("Kaavoitusteema"), self.theme_widget)
+
+ # Or e.g. "into_regulation"
+ def into_model(self) -> Regulation:
+ return Regulation(
+ config=self.config,
+ value=self.value_widget.get_value() if self.value_widget else None,
+ regulation_number=self.regulation_number_widget.get_value() if self.regulation_number_widget else None,
+ additional_information={
+ name: widget.get_value() for name, widget in self.additional_information_widgets.items() if widget
+ },
+ files=[file.filePath() for file in self.file_widgets],
+ theme=self.theme_widget.get_value() if self.theme_widget else None,
+ topic_tag=self.topic_tag_widget.get_value() if self.topic_tag_widget else None,
+ id_=self.regulation.id_,
+ )
diff --git a/arho_feature_template/gui/template_attribute_form.py b/arho_feature_template/gui/template_attribute_form.py
index 2508cd8..5f357f4 100644
--- a/arho_feature_template/gui/template_attribute_form.py
+++ b/arho_feature_template/gui/template_attribute_form.py
@@ -15,21 +15,22 @@
QScrollArea,
QSizePolicy,
QSpacerItem,
+ QTextEdit,
QTreeWidget,
QTreeWidgetItem,
)
-from arho_feature_template.core.plan_regulation_group_config import (
- PlanRegulationGroupDefinition,
- PlanRegulationGroupLibrary,
-)
-from arho_feature_template.gui.plan_regulation_group_widget import PlanRegulationGroupWidget
+from arho_feature_template.core.models import PlanFeature, RegulationGroup, RegulationGroupLibrary
+from arho_feature_template.gui.plan_regulation_group_widget import RegulationGroupWidget
+from arho_feature_template.project.layers.code_layers import UndergroundTypeLayer
from arho_feature_template.qgis_plugin_tools.tools.resources import resources_path
if TYPE_CHECKING:
+ from qgis.core import QgsGeometry
from qgis.PyQt.QtWidgets import QWidget
from arho_feature_template.core.template_library_config import FeatureTemplate
+ from arho_feature_template.gui.code_combobox import CodeComboBox
ui_path = resources.files(__package__) / "template_attribute_form.ui"
FormClass, _ = uic.loadUiType(ui_path)
@@ -38,14 +39,20 @@
class TemplateAttributeForm(QDialog, FormClass): # type: ignore
"""Parent class for feature template forms for adding and modifying feature attribute data."""
- def __init__(self, feature_template_config: FeatureTemplate):
+ def __init__(
+ self,
+ feature_template_config: FeatureTemplate,
+ geometry: QgsGeometry,
+ ):
super().__init__()
self.setupUi(self)
# TYPES
+ # self.model = None
+ self.geom = geometry
self.feature_name: QLineEdit
- self.feature_description: QLineEdit
- self.feature_underground: QLineEdit
+ self.feature_description: QTextEdit
+ self.feature_type_of_underground: CodeComboBox
self.plan_regulation_group_scrollarea: QScrollArea
self.plan_regulation_group_scrollarea_contents: QWidget
self.plan_regulation_group_libraries_combobox: QComboBox
@@ -53,8 +60,14 @@ def __init__(self, feature_template_config: FeatureTemplate):
self.button_box: QDialogButtonBox
# INIT
+ self.feature_type_of_underground.populate_from_code_layer(UndergroundTypeLayer)
+ self.feature_type_of_underground.removeItem(0) # Remove NULL from combobox as underground data is required
+ self.feature_type_of_underground.setCurrentIndex(1) # Set default to Maanpäällinen (index 1)
+
+ self.config = feature_template_config
+ self.regulation_group_widgets: list[RegulationGroupWidget] = []
self.scroll_area_spacer = None
- self.setWindowTitle(feature_template_config.name)
+ self.setWindowTitle(self.config.name)
self.init_plan_regulation_group_libraries()
# self.init_plan_regulation_groups_from_template(feature_template_config)
self.button_box.accepted.connect(self._on_ok_clicked)
@@ -72,36 +85,50 @@ def _remove_spacer(self):
def add_selected_plan_regulation_group(self, item: QTreeWidgetItem, column: int):
if not item.parent():
return
- definition: PlanRegulationGroupDefinition = item.data(column, Qt.UserRole)
- self.add_plan_regulation_group(definition)
+ regulation_group: RegulationGroup = item.data(column, Qt.UserRole)
+ self.add_plan_regulation_group(regulation_group)
- def add_plan_regulation_group(self, definition: PlanRegulationGroupDefinition):
- new_plan_regulation_group = PlanRegulationGroupWidget(definition)
- new_plan_regulation_group.delete_signal.connect(self.remove_plan_regulation_group)
+ def add_plan_regulation_group(self, definition: RegulationGroup):
+ regulation_group_widget = RegulationGroupWidget(definition)
+ regulation_group_widget.delete_signal.connect(self.remove_plan_regulation_group)
self._remove_spacer()
- self.plan_regulation_group_scrollarea_contents.layout().addWidget(new_plan_regulation_group)
+ self.plan_regulation_group_scrollarea_contents.layout().addWidget(regulation_group_widget)
+ self.regulation_group_widgets.append(regulation_group_widget)
self._add_spacer()
- def remove_plan_regulation_group(self, plan_regulation_group_widget: PlanRegulationGroupWidget):
- self.plan_regulation_group_scrollarea_contents.layout().removeWidget(plan_regulation_group_widget)
- plan_regulation_group_widget.deleteLater()
+ def remove_plan_regulation_group(self, regulation_group_widget: RegulationGroupWidget):
+ self.plan_regulation_group_scrollarea_contents.layout().removeWidget(regulation_group_widget)
+ self.regulation_group_widgets.remove(regulation_group_widget)
+ regulation_group_widget.deleteLater()
def init_plan_regulation_group_libraries(self):
katja_asemakaava_path = Path(os.path.join(resources_path(), "katja_asemakaava.yaml"))
- libraries = [PlanRegulationGroupLibrary.new_from_file(katja_asemakaava_path)]
+ libraries = [RegulationGroupLibrary.from_config_file(katja_asemakaava_path)]
for library in libraries:
self.init_plan_regulation_group_library(library)
- def init_plan_regulation_group_library(self, library: PlanRegulationGroupLibrary):
- self.plan_regulation_group_libraries_combobox.addItem(library.meta.name)
- for category in library.plan_regulation_group_categories:
+ def init_plan_regulation_group_library(self, library: RegulationGroupLibrary):
+ self.plan_regulation_group_libraries_combobox.addItem(library.name)
+ for category in library.regulation_group_categories:
category_item = QTreeWidgetItem()
category_item.setText(0, category.name)
self.plan_regulation_groups_tree.addTopLevelItem(category_item)
- for group_definition in category.plan_regulation_groups:
+ for group_definition in category.regulation_groups:
regulation_group_item = QTreeWidgetItem(category_item)
regulation_group_item.setText(0, group_definition.name)
regulation_group_item.setData(0, Qt.UserRole, group_definition)
+ def into_model(self) -> PlanFeature:
+ return PlanFeature(
+ name=self.feature_name.text(),
+ type_of_underground_id=self.feature_type_of_underground.value(),
+ description=self.feature_description.toPlainText(),
+ geom=self.geom,
+ layer_name=self.config.group,
+ regulation_groups=[reg_group_widget.into_model() for reg_group_widget in self.regulation_group_widgets],
+ id_=None,
+ )
+
def _on_ok_clicked(self):
+ self.model = self.into_model()
self.accept()
diff --git a/arho_feature_template/gui/template_attribute_form.ui b/arho_feature_template/gui/template_attribute_form.ui
index e4764e2..d3c58c5 100644
--- a/arho_feature_template/gui/template_attribute_form.ui
+++ b/arho_feature_template/gui/template_attribute_form.ui
@@ -20,7 +20,7 @@
Kaavakohteen tiedot
- true
+ false
-
@@ -40,27 +40,6 @@
- -
-
-
- Maanalaisuus
-
-
-
- -
-
-
-
-
- Maanpäällinen
-
-
- -
-
- Maanalainen
-
-
-
-
-
@@ -71,6 +50,16 @@
+ -
+
+
+ Maanalaisuus
+
+
+
+ -
+
+
@@ -172,7 +161,7 @@
0
0
596
- 600
+ 325
@@ -205,6 +194,11 @@
QLineEdit
+
+ CodeComboBox
+ QComboBox
+ arho_feature_template.gui.code_combobox
+
diff --git a/arho_feature_template/project/layers/code_layers.py b/arho_feature_template/project/layers/code_layers.py
index 7aa523f..2b14afa 100644
--- a/arho_feature_template/project/layers/code_layers.py
+++ b/arho_feature_template/project/layers/code_layers.py
@@ -18,4 +18,20 @@ class OrganisationLayer(AbstractCodeLayer):
name = "Toimija"
+class UndergroundTypeLayer(AbstractCodeLayer):
+ name = "Maanalaisuuden tyyppi"
+
+
+class PlanRegulationGroupTypeLayer(AbstractCodeLayer):
+ name = "Kaavamääräysryhmän tyyppi"
+
+ @classmethod
+ def get_id_of_regulation_type(cls, regulation_type: str) -> str | None:
+ for feature in cls.get_from_project().getFeatures():
+ if feature["value"] == regulation_type:
+ return feature["id"]
+
+ return None
+
+
code_layers = AbstractCodeLayer.__subclasses__()
diff --git a/arho_feature_template/project/layers/plan_layers.py b/arho_feature_template/project/layers/plan_layers.py
index a6514e6..9245251 100644
--- a/arho_feature_template/project/layers/plan_layers.py
+++ b/arho_feature_template/project/layers/plan_layers.py
@@ -2,11 +2,12 @@
import logging
from abc import abstractmethod
+from numbers import Number
from string import Template
from textwrap import dedent
from typing import TYPE_CHECKING, Any, ClassVar
-from qgis.core import QgsFeature, QgsVectorLayerUtils
+from qgis.core import NULL, QgsExpressionContextUtils, QgsFeature, QgsProject, QgsVectorLayerUtils
from qgis.utils import iface
from arho_feature_template.exceptions import LayerEditableError
@@ -15,7 +16,7 @@
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
- from arho_feature_template.core.models import Plan
+ from arho_feature_template.core.models import Plan, PlanFeature, Regulation, RegulationGroup
class AbstractPlanLayer(AbstractLayer):
@@ -72,27 +73,49 @@ def feature_from_model(cls, model: Plan) -> QgsFeature:
return feature
-class LandUsePointLayer(AbstractPlanLayer):
+class PlanFeatureLayer(AbstractPlanLayer):
+ @classmethod
+ def feature_from_model(cls, model: PlanFeature, plan_id: str | None = None) -> QgsFeature:
+ layer = cls.get_from_project()
+
+ if not model.geom:
+ message = "Plan feature must have a geometry to be added to the layer"
+ raise ValueError(message)
+
+ feature = QgsVectorLayerUtils.createFeature(layer, model.geom)
+ feature["name"] = {"fin": model.name if model.name else ""}
+ feature["type_of_underground_id"] = model.type_of_underground_id
+ feature["description"] = {"fin": model.description if model.description else ""}
+ feature["plan_id"] = (
+ plan_id
+ if plan_id
+ else QgsExpressionContextUtils.projectScope(QgsProject.instance()).variable("active_plan_id")
+ )
+
+ return feature
+
+
+class LandUsePointLayer(PlanFeatureLayer):
name = "Maankäytön kohteet"
filter_template = Template("plan_id = '$plan_id'")
-class OtherPointLayer(AbstractPlanLayer):
+class OtherPointLayer(PlanFeatureLayer):
name = "Muut pisteet"
filter_template = Template("plan_id = '$plan_id'")
-class LineLayer(AbstractPlanLayer):
+class LineLayer(PlanFeatureLayer):
name = "Viivat"
filter_template = Template("plan_id = '$plan_id'")
-class LandUseAreaLayer(AbstractPlanLayer):
+class LandUseAreaLayer(PlanFeatureLayer):
name = "Aluevaraus"
filter_template = Template("plan_id = '$plan_id'")
-class OtherAreaLayer(AbstractPlanLayer):
+class OtherAreaLayer(PlanFeatureLayer):
name = "Osa-alue"
filter_template = Template("plan_id = '$plan_id'")
@@ -101,6 +124,21 @@ class RegulationGroupLayer(AbstractPlanLayer):
name = "Kaavamääräysryhmät"
filter_template = Template("plan_id = '$plan_id'")
+ @classmethod
+ def feature_from_model(cls, model: RegulationGroup, plan_id: str | None = None) -> QgsFeature:
+ layer = cls.get_from_project()
+
+ feature = QgsVectorLayerUtils.createFeature(layer)
+ feature["short_name"] = model.short_name if model.short_name else None
+ feature["name"] = {"fin": model.name}
+ feature["type_of_plan_regulation_group_id"] = model.type_code_id
+ feature["plan_id"] = (
+ plan_id
+ if plan_id
+ else QgsExpressionContextUtils.projectScope(QgsProject.instance()).variable("active_plan_id")
+ )
+ return feature
+
class RegulationGroupAssociationLayer(AbstractPlanLayer):
name = "Kaavamääräysryhmien assosiaatiot"
@@ -117,6 +155,30 @@ class RegulationGroupAssociationLayer(AbstractPlanLayer):
)
)
+ layer_name_to_attribute_map: ClassVar[dict[str, str]] = {
+ LandUsePointLayer.name: "land_use_point_id",
+ OtherAreaLayer.name: "other_area_id",
+ OtherPointLayer.name: "other_point_id",
+ LandUseAreaLayer.name: "land_use_area_id",
+ LineLayer.name: "line_id",
+ PlanLayer.name: "plan_id",
+ }
+
+ @classmethod
+ def feature_from(cls, regulation_group_id: str, layer_name: str, feature_id: str) -> QgsFeature:
+ layer = cls.get_from_project()
+
+ feature = QgsVectorLayerUtils.createFeature(layer)
+ feature["plan_regulation_group_id"] = regulation_group_id
+
+ attribute = cls.layer_name_to_attribute_map.get(layer_name)
+ if not attribute:
+ msg = f"Unrecognized layer name given for saving regulation group association: {layer_name}"
+ raise ValueError(msg)
+ feature[attribute] = feature_id
+
+ return feature
+
class PlanRegulationLayer(AbstractPlanLayer):
name = "Kaavamääräys"
@@ -133,6 +195,22 @@ class PlanRegulationLayer(AbstractPlanLayer):
)
)
+ @classmethod
+ def feature_from_model(cls, model: Regulation) -> QgsFeature:
+ layer = cls.get_from_project()
+
+ feature = QgsVectorLayerUtils.createFeature(layer)
+ feature["plan_regulation_group_id"] = model.regulation_group_id_
+ feature["type_of_plan_regulation_id"] = model.config.id
+ feature["unit"] = model.config.unit
+ feature["text_value"] = {"fin": model.value if isinstance(model.value, str) else ""}
+ feature["numeric_value"] = model.value if isinstance(model.value, Number) else NULL
+ feature["name"] = {"fin": model.topic_tag if model.topic_tag else ""}
+ # feature["plan_theme_id"]
+ # feature["type_of_verbal_plan_regulation_id"]
+
+ return feature
+
class PlanPropositionLayer(AbstractPlanLayer):
name = "Kaavasuositus"
@@ -161,3 +239,5 @@ class SourceDataLayer(AbstractPlanLayer):
plan_layers = AbstractPlanLayer.__subclasses__()
+plan_layers.remove(PlanFeatureLayer)
+plan_layers.extend(PlanFeatureLayer.__subclasses__())
diff --git a/arho_feature_template/resources/katja_asemakaava.yaml b/arho_feature_template/resources/katja_asemakaava.yaml
index 63910c9..58bf237 100644
--- a/arho_feature_template/resources/katja_asemakaava.yaml
+++ b/arho_feature_template/resources/katja_asemakaava.yaml
@@ -1,6 +1,5 @@
-meta:
- name: Asemakaavan kaavamääräysryhmät (Katja)
- version: 1
+name: Asemakaavan kaavamääräysryhmät (Katja)
+version: 1
categories:
- category_code: aluevaraukset
@@ -10,7 +9,7 @@ categories:
- name: Asuinrakennusten alue
geometry: Alue
color_code: #000000
- letter_code: A
+ short_name: A
plan_regulations:
- regulation_code: asumisenAlue
additional_information:
@@ -19,7 +18,7 @@ categories:
- name: Asuinkerrostalojen alue
geometry: Alue
color_code: #000000
- letter_code: AK
+ short_name: AK
plan_regulations:
- regulation_code: asuinkerrostaloalue
additional_information:
@@ -28,7 +27,7 @@ categories:
- name: Asuinpientalojen alue
geometry: Alue
color_code: #A9D08E
- letter_code: AP
+ short_name: AP
plan_regulations:
- regulation_code: asuinpientaloalue
additional_information:
@@ -37,7 +36,7 @@ categories:
- name: Rivitalojen ja muiden kytkettyjen asuinrakennusten alue
geometry: Alue
color_code: #FFD966
- letter_code: AR
+ short_name: AR
plan_regulations:
- regulation_code: rivitalojenJaMuidenKytkettyjenAsuinpientalojenAlue
additional_information:
@@ -46,7 +45,7 @@ categories:
- name: Erillispientalojen alue
geometry: Alue
color_code: #E06666
- letter_code: AO
+ short_name: AO
plan_regulations:
- regulation_code: erillistenAsuinpientalojenAlue
additional_information:
@@ -55,7 +54,7 @@ categories:
- name: Asuin-, liike- ja toimistorakennusten alue
geometry: Alue
color_code: #5B9BD5
- letter_code: AL
+ short_name: AL
plan_regulations:
- regulation_code: asumisenAlue
additional_information:
@@ -70,7 +69,7 @@ categories:
- name: Asumista palveleva yhteiskäyttöinen alue
geometry: Alue
color_code: #9BC2E6
- letter_code: AH
+ short_name: AH
plan_regulations:
- regulation_code: asumistaPalvelevaYhteiskayttoinenAlue
additional_information:
@@ -79,7 +78,7 @@ categories:
- name: Maatilojen talouskeskusten alue
geometry: Alue
color_code: #FFD966
- letter_code: AM
+ short_name: AM
plan_regulations:
- regulation_code: maatilanTalouskeskuksenAlue
additional_information:
@@ -88,7 +87,7 @@ categories:
- name: Keskustatoimintojen alue
geometry: Alue
color_code: #D9D9D9
- letter_code: C
+ short_name: C
plan_regulations:
- regulation_code: keskustatoimintojenAlue
additional_information:
@@ -97,7 +96,7 @@ categories:
- name: Yleisten rakennusten alue
geometry: Alue
color_code: #C6E0B4
- letter_code: Y
+ short_name: Y
plan_regulations:
- regulation_code: yleistenRakennustenAlue
additional_information:
@@ -107,7 +106,7 @@ categories:
- name: Palvelurakennusten alue
geometry: Alue
color_code: #FFC000
- letter_code: P
+ short_name: P
plan_regulations:
- regulation_code: palvelujenAlue
additional_information:
@@ -116,7 +115,7 @@ categories:
- name: Teollisuus- ja varastorakennusten alue
geometry: Alue
color_code: #70AD47
- letter_code: T
+ short_name: T
plan_regulations:
- regulation_code: teollisuusalue
additional_information:
@@ -128,7 +127,7 @@ categories:
- name: Teollisuusrakennusten alue, jolla ympäristö asettaa toiminnan laadulle erityisiä vaatimuksia
geometry: Alue
color_code: #5B9BD5
- letter_code: TY
+ short_name: TY
plan_regulations:
- regulation_code: teollisuusalue
additional_information:
@@ -138,7 +137,7 @@ categories:
- name: Puisto
geometry: Alue
color_code: #4472C4
- letter_code: VP
+ short_name: VP
plan_regulations:
- regulation_code: puisto
additional_information:
@@ -147,7 +146,7 @@ categories:
- name: Leikkipuisto
geometry: Alue
color_code: #A9D08E
- letter_code: VK
+ short_name: VK
plan_regulations:
- regulation_code: leikkipuisto
additional_information:
@@ -156,7 +155,7 @@ categories:
- name: Uimaranta-alue
geometry: Alue
color_code: #E06666
- letter_code: VV
+ short_name: VV
plan_regulations:
- regulation_code: uimaranta
additional_information:
@@ -165,7 +164,7 @@ categories:
- name: Vapaa-ajan asumisen ja matkailun alue
geometry: Alue
color_code: #70AD47
- letter_code: R
+ short_name: R
plan_regulations:
- regulation_code: vapaaAjanAsumisenJaMatkailunAlue
additional_information:
@@ -174,7 +173,7 @@ categories:
- name: Maisemallisesti arvokas peltoalue
geometry: Alue
color_code: #abcdef
- letter_code: MA
+ short_name: MA
plan_regulations:
- regulation_code: pelto
additional_information:
@@ -185,7 +184,7 @@ categories:
- name: Virkistysalue
geometry: Alue
color_code: #FFC000
- letter_code: V
+ short_name: V
plan_regulations:
- regulation_code: virkistysalue
additional_information:
@@ -197,7 +196,7 @@ categories:
- name: Auton säilytyspaikan rakennusala
geometry: Alue
- letter_code: a
+ short_name: a
plan_regulations:
- regulation_code: rakennusala
additional_information:
@@ -207,7 +206,7 @@ categories:
- name: Rakennusala, jolle saa sijoittaa lasten päiväkodin
geometry: Alue
- letter_code: pk
+ short_name: pk
plan_regulations:
- regulation_code: rakennusala
additional_information:
@@ -217,7 +216,7 @@ categories:
- name: Rakennusala, jolle saa sijoittaa saunan
geometry: Alue
- letter_code: sa
+ short_name: sa
plan_regulations:
- regulation_code: rakennusalaJolleSaaSijoittaaSaunan
additional_information:
@@ -225,7 +224,7 @@ categories:
- name: Rakennetun kulttuuriympäristön ja maiseman vaalimisen kannalta tärkeä alue
geometry: Alue
- letter_code: kyma
+ short_name: kyma
plan_regulations:
- regulation_code: maisemallisestiArvokasAlue
additional_information:
@@ -236,7 +235,7 @@ categories:
- name: Maisemallisesti arvokas alue
geometry: Alue
- letter_code: ma
+ short_name: ma
plan_regulations:
- regulation_code: maisemallisestiArvokasAlue
additional_information:
@@ -244,7 +243,7 @@ categories:
- name: Kansainvälisesti arvokas maisema-alue
geometry: Alue
- letter_code: kvma
+ short_name: kvma
plan_regulations:
- regulation_code: maisemallisestiArvokasAlue
additional_information:
@@ -275,3 +274,8 @@ categories:
geometry: Alue
plan_regulations:
- regulation_code: kattokaltevuus
+
+ - name: Kadun tai tien nimi
+ geometry: Alue
+ plan_regulations:
+ - regulation_code: kadunTaiTienNimi