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
qgsfilterlineedit.h
+ + 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