1
1
from __future__ import annotations
2
2
3
+ import logging
4
+ import os
3
5
from dataclasses import dataclass , field
4
6
from enum import Enum
7
+ from pathlib import Path
5
8
from typing import TYPE_CHECKING
6
9
10
+ import yaml
11
+
12
+ from arho_feature_template .exceptions import ConfigSyntaxError
13
+ from arho_feature_template .qgis_plugin_tools .tools .resources import resources_path
14
+ from arho_feature_template .utils .misc_utils import get_layer_by_name , iface
15
+
7
16
if TYPE_CHECKING :
8
- from numbers import Number
9
- from qgis .core import QgsGeometry , QgsFeature
17
+ from typing import Literal
18
+
19
+ from qgis .core import QgsFeature , QgsGeometry
20
+
21
+
22
+ logger = logging .getLogger (__name__ )
23
+
24
+ DEFAULT_PLAN_REGULATIONS_CONFIG_PATH = Path (os .path .join (resources_path (), "kaavamaaraykset.yaml" ))
10
25
11
26
12
27
class ValueType (Enum ):
@@ -17,13 +32,145 @@ class ValueType(Enum):
17
32
VERSIONED_TEXT = "kieliversioitu teksti"
18
33
19
34
35
+ @dataclass
36
+ class RegulationGroupCategory :
37
+ category_code : str
38
+ name : str | None
39
+ regulation_groups : list [RegulationGroup ]
40
+
41
+ @classmethod
42
+ def from_config_data (cls , data : dict ) -> RegulationGroupCategory :
43
+ return cls (
44
+ category_code = data ["category_code" ],
45
+ name = data .get ("name" ),
46
+ regulation_groups = [
47
+ RegulationGroup .from_config_data (config_data ) for config_data in data ["plan_regulation_groups" ]
48
+ ],
49
+ )
50
+
51
+
52
+ @dataclass
53
+ class RegulationGroupLibrary :
54
+ """Describes the configuration of a plan regulation group library"""
55
+
56
+ name : str
57
+ version : int | None
58
+ description : str | None
59
+ regulation_group_categories : list [RegulationGroupCategory ]
60
+ # regulation_groups: list[RegulationGroup]
61
+
62
+ @classmethod
63
+ def from_config_file (cls , config_fp : Path ) -> RegulationGroupLibrary :
64
+ with config_fp .open (encoding = "utf-8" ) as f :
65
+ data = yaml .safe_load (f )
66
+ return RegulationGroupLibrary (
67
+ name = data ["name" ],
68
+ version = data .get ("version" ),
69
+ description = data .get ("description" ),
70
+ regulation_group_categories = [
71
+ RegulationGroupCategory .from_config_data (category ) for category in data ["categories" ]
72
+ ],
73
+ )
74
+
75
+
76
+ @dataclass
77
+ class RegulationLibrary :
78
+ """Describes the set of plan regulations."""
79
+
80
+ version : str
81
+ regulations : list [RegulationConfig ]
82
+ regulations_dict : dict [str , RegulationConfig ]
83
+
84
+ _instance : RegulationLibrary | None = None
85
+
86
+ @classmethod
87
+ def get_instance (cls ) -> RegulationLibrary :
88
+ """Get the singleton instance, if initialized."""
89
+ if cls ._instance is None :
90
+ return cls .initialize ()
91
+ return cls ._instance
92
+
93
+ @classmethod
94
+ def get_regulations (cls ) -> list [RegulationConfig ]:
95
+ """Get the list of top-level regulation configs, if instance is initialized."""
96
+ return cls .get_instance ().regulations
97
+
98
+ @classmethod
99
+ def get_regulations_dict (cls ) -> dict [str , RegulationConfig ]:
100
+ """Get all regulations in a dictionary where keys are regulations codes and values RegulationConfigs."""
101
+ return cls .get_instance ().regulations_dict
102
+
103
+ @classmethod
104
+ def get_regulation_by_code (cls , regulation_code : str ) -> RegulationConfig | None :
105
+ """Get a regulation by it's regulation code (if exists)."""
106
+ return cls .get_instance ().regulations_dict .get (regulation_code )
107
+
108
+ @classmethod
109
+ def initialize (
110
+ cls ,
111
+ config_path : Path = DEFAULT_PLAN_REGULATIONS_CONFIG_PATH ,
112
+ type_of_plan_regulations_layer_name = "Kaavamääräyslaji" ,
113
+ language : Literal ["fin" , "eng" , "swe" ] = "fin" ,
114
+ ) -> RegulationLibrary :
115
+ # Initialize RegulationLibrary and RegulationConfigs from QGIS layer and config file
116
+
117
+ # 1. Read config file into a dict
118
+ with config_path .open (encoding = "utf-8" ) as f :
119
+ config_data = yaml .safe_load (f )
120
+
121
+ # 2. Read code layer
122
+ layer = get_layer_by_name (type_of_plan_regulations_layer_name )
123
+ if layer is None :
124
+ msg = f"Could not find layer { type_of_plan_regulations_layer_name } !"
125
+ raise KeyError (msg )
126
+
127
+ # 3. Initialize regulation configs from layer. Storing them by their ID is handy for adding childs later
128
+ id_to_regulation_map : dict [str , RegulationConfig ] = {
129
+ feature ["id" ]: RegulationConfig .from_feature (feature , language ) for feature in layer .getFeatures ()
130
+ }
131
+
132
+ # 4. Add information from config file (value, unit, category only) and link child regulations
133
+ try :
134
+ regulation_data : dict = {data ["regulation_code" ]: data for data in config_data ["plan_regulations" ]}
135
+ top_level_regulations : list [RegulationConfig ] = []
136
+ for regulation_config in id_to_regulation_map .values ():
137
+ # Add possible information from config data file
138
+ data = regulation_data .get (regulation_config .regulation_code )
139
+ if data :
140
+ regulation_config .category_only = data .get ("category_only" , False )
141
+ regulation_config .value_type = ValueType (data ["value_type" ]) if "value_type" in data else None
142
+ regulation_config .unit = data ["unit" ] if "unit" in data else None
143
+
144
+ # Top-level, add to list
145
+ if not regulation_config .parent_id :
146
+ top_level_regulations .append (regulation_config )
147
+ else :
148
+ # Add as child of another regulation
149
+ id_to_regulation_map [regulation_config .parent_id ].child_regulations .append (regulation_config )
150
+ except KeyError as e :
151
+ raise ConfigSyntaxError (str (e )) from e
152
+
153
+ # 5. Create dictionary, useful when creating PlanRegulationDefinitions at least
154
+ regulations_dict : dict [str , RegulationConfig ] = {}
155
+ for reg in top_level_regulations :
156
+ reg .add_to_dictionary (regulations_dict )
157
+
158
+ # 5. Create instance
159
+ cls ._instance = cls (
160
+ version = config_data ["version" ], regulations = top_level_regulations , regulations_dict = regulations_dict
161
+ )
162
+ logger .info ("RegulationLibrary initialized successfully." )
163
+ return cls ._instance
164
+
165
+
20
166
@dataclass
21
167
class RegulationConfig :
22
168
"""
23
169
Describes plan regulation type.
24
-
170
+
25
171
Initialized from DB/QGIS layer and extended with data from a config file.
26
172
"""
173
+
27
174
id : str
28
175
regulation_code : str
29
176
name : str
@@ -66,10 +213,10 @@ def add_to_dictionary(self, dictionary: dict[str, RegulationConfig]):
66
213
@dataclass
67
214
class Regulation :
68
215
config : RegulationConfig # includes regulation_code and unit among other needed data for saving feature
69
- value : str | Number | tuple [int , int ] | None = None
70
- regulation_number : int | None
71
- additional_information : dict [ str , str | Number | None ]
72
- files : list [str ] = []
216
+ value : str | float | int | tuple [int , int ] | None = None
217
+ additional_information : dict [ str , str | float | int | None ] | None = None
218
+ regulation_number : int | None = None
219
+ files : list [str ] = field ( default_factory = list )
73
220
theme : str | None = None
74
221
topic_tag : str | None = None
75
222
id_ : int | None = None
@@ -80,14 +227,52 @@ class Regulation:
80
227
81
228
@dataclass
82
229
class RegulationGroup :
83
- type_code : str
230
+ type_code : str | None
84
231
name : str | None
85
232
short_name : str | None
86
233
color_code : str | None
87
234
letter_code : str | None
88
- regulations : list [Regulation ] | list [ RegulationConfig ]
235
+ regulations : list [Regulation ]
89
236
id_ : int | None = None
90
237
238
+ @classmethod
239
+ def from_config_data (cls , data : dict ) -> RegulationGroup :
240
+ regulations = []
241
+ for reg_data in data ["plan_regulations" ]:
242
+ reg_code = reg_data ["regulation_code" ]
243
+ config = RegulationLibrary .get_regulation_by_code (reg_code )
244
+ if config :
245
+ info_data = reg_data .get ("additional_information" )
246
+ regulations .append (
247
+ Regulation (
248
+ config = config ,
249
+ value = reg_data .get ("value" ),
250
+ additional_information = {info ["type" ]: info .get ("value" ) for info in info_data }
251
+ if info_data
252
+ else None ,
253
+ regulation_number = reg_data .get ("regulation_number" ),
254
+ files = reg_data .get ("files" ) if reg_data .get ("files" ) else [],
255
+ theme = reg_data .get ("theme" ),
256
+ topic_tag = reg_data .get ("topic_tag" ),
257
+ id_ = None ,
258
+ )
259
+ )
260
+ else :
261
+ iface .messageBar ().pushWarning ("" , f"Could not find plan regulation { reg_code } !" )
262
+ return cls (
263
+ type_code = None ,
264
+ name = data .get ("name" ),
265
+ short_name = data .get ("color_code" ),
266
+ color_code = data .get ("color_code" ),
267
+ letter_code = data .get ("letter_code" ),
268
+ regulations = regulations ,
269
+ id_ = None ,
270
+ )
271
+
272
+ # @classmethod
273
+ # def from_database(cls) -> RegulationGroup:
274
+ # pass
275
+
91
276
92
277
@dataclass
93
278
class Plan :
0 commit comments